Files
Profa fd2f37eaa0 fix(v3): update README claims and init hooks to use correct package name
Changes:
- Fix README.md agent counts (48 QE agents = 41 main + 7 subagents)
- Fix README.md skill counts (15 QE-specific skills)
- Fix v3/README.md agent counts (47 → 48)
- Fix init-wizard.ts hooks from 'npx @agentic-qe/v3' to 'npx agentic-qe'
- Complete v2 → v2/ folder reorganization (54 items moved)
- Update root package.json to point to v3 as main version

Verified claims:
- 48 QE agents (41 main + 7 TDD subagents)
- 15 QE-specific skills
- 12 DDD bounded contexts
- ~5,600+ tests
- HNSW, ReasoningBank, Queen Coordinator features exist

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 18:03:40 +00:00

1621 lines
54 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* LearningEngine - Phase 2 (Milestone 2.2)
*
* Implements reinforcement learning for agent performance improvement.
* Uses Q-learning algorithm to optimize task execution strategies.
*/
import { v4 as uuidv4 } from 'uuid';
import { Logger } from '../utils/Logger';
import { SwarmMemoryManager } from '../core/memory/SwarmMemoryManager';
import { QLearning, QLearningConfig } from './QLearning';
import { SARSALearner, SARSAConfig } from './algorithms/SARSALearner';
import { ActorCriticLearner, createDefaultActorCriticConfig } from './algorithms/ActorCriticLearner';
import { PPOLearner, createDefaultPPOConfig } from './algorithms/PPOLearner';
import { AbstractRLLearner } from './algorithms/AbstractRLLearner';
import { StateExtractor } from './StateExtractor';
import { RewardCalculator, TaskResult } from './RewardCalculator';
import packageJson from '../../package.json';
import {
recordLearningEpisode,
recordQValueUpdate,
recordPatternMatch,
recordAlgorithmSwitch,
recordExperienceSharing,
updateSharingConnections,
updateGaugeValues,
recordLearningSession,
withLearningSpan,
} from '../telemetry/LearningTelemetry';
const PACKAGE_VERSION = packageJson.version;
import {
LearningConfig,
TaskExperience,
TaskState,
AgentAction,
LearningFeedback,
LearningOutcome,
LearnedPattern,
LearningModelState,
FailurePattern,
StrategyRecommendation,
LearningEvent,
PerformanceMetrics
} from './types';
import { QETask } from '../types';
import { ExperienceSharingProtocol, SharedExperience } from './ExperienceSharingProtocol';
import { HNSWPatternAdapter, HNSWPatternAdapterConfig, PatternSearchResult } from './HNSWPatternAdapter';
/**
* RL Algorithm type selection
*/
export type RLAlgorithmType = 'q-learning' | 'sarsa' | 'actor-critic' | 'ppo' | 'maml' | 'legacy';
/**
* Extended learning configuration with algorithm selection
*/
export interface ExtendedLearningConfig extends LearningConfig {
algorithm?: RLAlgorithmType; // Which RL algorithm to use
enableExperienceSharing?: boolean; // Enable cross-agent experience sharing
experienceSharingPriority?: number; // Minimum priority for sharing (0-1)
/** Enable HNSW vector search for O(log n) pattern matching (Phase 0 M0.3) */
enableHNSW?: boolean;
/** HNSW adapter configuration */
hnswConfig?: HNSWPatternAdapterConfig;
}
/**
* Default learning configuration
*/
const DEFAULT_CONFIG: ExtendedLearningConfig = {
enabled: true,
learningRate: 0.1,
discountFactor: 0.95,
explorationRate: 0.3,
explorationDecay: 0.995,
minExplorationRate: 0.01,
maxMemorySize: 100 * 1024 * 1024, // 100MB
batchSize: 32,
updateFrequency: 10,
algorithm: 'q-learning', // Default to Q-Learning
enableExperienceSharing: false, // Disabled by default for backward compatibility
experienceSharingPriority: 0.5, // Only share moderately valuable experiences
enableHNSW: false, // Disabled by default for backward compatibility (Phase 0 M0.3)
};
/**
* LearningEngine - Reinforcement learning for agents
*/
export class LearningEngine {
private readonly logger: Logger;
private readonly memoryStore: SwarmMemoryManager;
private readonly agentId: string;
private config: ExtendedLearningConfig;
private qTable: Map<string, Map<string, number>>; // state-action values (legacy)
private rlAlgorithm?: AbstractRLLearner; // Current RL algorithm (Q-Learning, SARSA, etc.)
private qLearning?: QLearning; // Q-learning integration (backward compatibility)
private useQLearning: boolean;
private experiences: TaskExperience[];
// REMOVED: private patterns: Map<string, LearnedPattern>; (now persisted via memoryStore.storePattern)
private failurePatterns: Map<string, FailurePattern>;
private taskCount: number;
private readonly stateExtractor: StateExtractor;
private readonly rewardCalculator: RewardCalculator;
private experienceSharing?: ExperienceSharingProtocol;
// Phase 0 M0.3: HNSW vector search for O(log n) pattern matching
private hnswAdapter?: HNSWPatternAdapter;
constructor(
agentId: string,
memoryStore: SwarmMemoryManager,
config: Partial<ExtendedLearningConfig> = {}
) {
this.logger = Logger.getInstance();
this.agentId = agentId;
this.memoryStore = memoryStore;
this.config = { ...DEFAULT_CONFIG, ...config };
this.qTable = new Map();
this.useQLearning = false; // Default to legacy implementation
this.experiences = [];
// REMOVED: this.patterns = new Map(); (now using memoryStore.storePattern for persistence)
this.failurePatterns = new Map();
this.taskCount = 0;
this.stateExtractor = new StateExtractor();
this.rewardCalculator = new RewardCalculator();
// Initialize RL algorithm if specified
if (this.config.algorithm && this.config.algorithm !== 'legacy') {
this.setAlgorithm(this.config.algorithm);
}
// Phase 0 M0.3: Initialize HNSW adapter for O(log n) pattern matching
if (this.config.enableHNSW) {
this.hnswAdapter = new HNSWPatternAdapter(this.config.hnswConfig);
this.logger.info('HNSW pattern adapter created for O(log n) similarity search');
}
// UNIFIED PERSISTENCE ARCHITECTURE (v2.2.0):
// LearningEngine uses SwarmMemoryManager for ALL persistence.
// All data goes to the SINGLE database: .agentic-qe/memory.db
// This ensures CLI, MCP, and agents all share the same data.
//
// Key methods that persist to memory.db:
// - storeLearningExperience() -> learning_experiences table
// - storePattern() -> patterns table
// - upsertQValue() -> q_values table
this.logger.info(`LearningEngine initialized for agent ${agentId} with algorithm ${this.config.algorithm} - using ${memoryStore.constructor.name} for persistent storage`);
}
/**
* Initialize the learning engine
*
* Architecture: Uses shared memoryStore (SwarmMemoryManager) for all persistence.
* No database initialization needed - memoryStore handles the database connection.
*/
async initialize(): Promise<void> {
this.logger.info(`Initializing LearningEngine for agent ${this.agentId}`);
// Load previous learning state if exists
await this.loadState();
// Load Q-values from memoryStore if available
// Q-table loading done inline above
// Store config in memory via reasoningBank's memoryStore
if (this.memoryStore) {
// Serialize config to Record<string, unknown> for type safety
const serializableConfig: Record<string, unknown> = {
...this.config
};
await this.memoryStore.store(
`phase2/learning/${this.agentId}/config`,
serializableConfig,
{ partition: 'learning' }
);
}
// Phase 0 M0.3: Initialize HNSW adapter for vector similarity search
if (this.hnswAdapter) {
await this.hnswAdapter.initialize();
this.logger.info('HNSW pattern adapter initialized for O(log n) similarity search');
}
this.logger.info('LearningEngine initialized successfully');
}
/**
* @deprecated Use learnFromExecution() instead. This method is maintained for backward compatibility.
* Will be removed in v2.0.0
*
* Record experience from task execution (DEPRECATED)
* Redirects to learnFromExecution() for consolidated implementation
*/
async recordExperience(task: unknown, result: TaskResult, feedback?: LearningFeedback): Promise<void> {
this.logger.warn('[LearningEngine] recordExperience() is deprecated. Use learnFromExecution() instead. This method will be removed in v2.0.0');
// Redirect to unified method
await this.learnFromExecution(task, result, feedback);
}
/**
* Load patterns from memoryStore (SwarmMemoryManager -> memory.db)
*
* Architecture: Retrieves patterns from the unified memory.db database
* via SwarmMemoryManager.
*
* Note: This method is legacy and not currently used (patterns loaded on-demand).
*/
private async loadPatternsFromMemoryStore(): Promise<void> {
try {
// Query all patterns from memory.db (confidence >= 0 returns all)
const allPatterns = await this.memoryStore.queryPatternsByConfidence(0);
if (allPatterns.length === 0) {
this.logger.info('No existing patterns found in memory.db');
return;
}
this.logger.info(`Found ${allPatterns.length} patterns in memory.db for agent ${this.agentId}`);
} catch (error) {
this.logger.warn(`Failed to load patterns from memory.db:`, error);
}
}
/**
* Learn from task execution with automatic database persistence
* This is the primary method called by BaseAgent.onPostTask()
* Consolidates learnFromExecution() and recordExperience() functionality
*
* Architecture: Uses SwarmMemoryManager for all persistence (storeLearningExperience,
* upsertQValue, storeLearningSnapshot) instead of LearningPersistence adapter.
*/
async learnFromExecution(
task: unknown,
result: unknown,
feedback?: LearningFeedback
): Promise<LearningOutcome> {
if (!this.config.enabled) {
return this.createOutcome(false, 0, 0);
}
const episodeStartTime = Date.now();
try {
// Extract experience from task execution
const taskObj = (typeof task === 'object' && task !== null ? task : {}) as Record<string, unknown>;
const resultObj = (typeof result === 'object' && result !== null ? result : {}) as Record<string, unknown>;
const experience = this.extractExperience(taskObj, resultObj, feedback);
const reward = this.calculateReward(resultObj, feedback);
experience.reward = reward;
// Store in memory (fast)
this.experiences.push(experience);
// Update Q-table (in-memory)
await this.updateQTable(experience);
// Record telemetry: Q-value update occurred
recordQValueUpdate(this.agentId, 1, undefined, this.getAlgorithm());
// Persist to database via memoryStore (replaces persistence adapter)
// Only if memoryStore is SwarmMemoryManager
if (this.memoryStore && typeof (this.memoryStore as SwarmMemoryManager & { storeLearningExperience?: unknown }).storeLearningExperience === "function") {
try {
// Store experience
await this.memoryStore.storeLearningExperience({
agentId: this.agentId,
taskId: experience.taskId,
taskType: experience.taskType,
state: JSON.stringify(experience.state),
action: JSON.stringify(experience.action),
reward: experience.reward,
nextState: JSON.stringify(experience.nextState)
});
// Persist Q-value
const stateKey = this.stateExtractor.encodeState(experience.state);
const actionKey = this.stateExtractor.encodeAction(experience.action);
const stateActions = this.qTable.get(stateKey);
const qValue = stateActions?.get(actionKey) || 0;
await this.memoryStore.upsertQValue(
this.agentId,
stateKey,
actionKey,
qValue
);
} catch (persistError) {
this.logger.warn('Persistence failed, continuing with in-memory learning:', persistError);
// Don't fail learning due to persistence errors
}
}
// Update patterns
await this.updatePatterns(experience);
// Detect failure patterns
if (!resultObj.success) {
await this.detectFailurePattern(experience);
}
// Increment task count
this.taskCount++;
// Periodic batch update and snapshot
if (this.taskCount % this.config.updateFrequency === 0) {
await this.performBatchUpdate();
// Store learning snapshot via memoryStore
if (this.memoryStore && typeof (this.memoryStore as SwarmMemoryManager & { storeLearningExperience?: unknown }).storeLearningExperience === "function") {
const improvement = await this.calculateImprovement();
await this.memoryStore.storeLearningSnapshot({
agentId: this.agentId,
snapshotType: 'performance',
metrics: {
qTableSize: this.qTable.size
},
totalExperiences: this.experiences.length,
improvementRate: improvement.improvementRate,
explorationRate: this.config.explorationRate
});
}
}
// Decay exploration rate
this.decayExploration();
// Calculate improvement
const improvement = await this.calculateImprovement();
// Share experience with peers if enabled and valuable
if (this.config.enableExperienceSharing && this.experienceSharing) {
await this.shareExperienceWithPeers(experience);
}
// Emit learning event
await this.emitLearningEvent('training', {
experience,
reward,
improvement
});
// Save state periodically
if (this.taskCount % 50 === 0) {
await this.saveState();
}
return improvement;
} catch (error) {
this.logger.error(`Learning from execution failed:`, error);
return this.createOutcome(false, 0, 0);
}
}
/**
* Recommend best strategy for a given state
*/
async recommendStrategy(state: TaskState): Promise<StrategyRecommendation> {
const stateKey = this.encodeState(state);
// Get Q-values for all actions in this state
const actionValues = this.qTable.get(stateKey) || new Map();
if (actionValues.size === 0) {
// No learned strategies yet, return default
return {
strategy: 'default',
confidence: 0.5,
expectedImprovement: 0,
reasoning: 'No learned strategies available yet',
alternatives: []
};
}
// Find best action
let bestAction = '';
let bestValue = -Infinity;
const alternatives: { strategy: string; confidence: number }[] = [];
for (const [action, value] of actionValues.entries()) {
if (value > bestValue) {
if (bestAction) {
alternatives.push({
strategy: bestAction,
confidence: this.valueToConfidence(bestValue)
});
}
bestValue = value;
bestAction = action;
} else {
alternatives.push({
strategy: action,
confidence: this.valueToConfidence(value)
});
}
}
// Sort alternatives by confidence
alternatives.sort((a, b) => b.confidence - a.confidence);
return {
strategy: bestAction,
confidence: this.valueToConfidence(bestValue),
expectedImprovement: Math.max(0, bestValue * 100),
reasoning: `Learned from ${this.experiences.length} experiences`,
alternatives: alternatives.slice(0, 3)
};
}
/**
* Get learned patterns for this agent
* Now queries from memoryStore (SwarmMemoryManager) with agent filtering
*/
async getPatterns(): Promise<LearnedPattern[]> {
try {
// Query patterns from memoryStore if available
if (!this.memoryStore || typeof this.memoryStore.queryPatternsByAgent !== 'function') {
return [];
}
// Use agent-specific query to avoid mixing patterns from different agents
const dbPatterns = await this.memoryStore.queryPatternsByAgent(this.agentId, 0);
return dbPatterns.map((p: { id?: string; pattern: string; confidence: number; metadata?: Record<string, unknown>; usageCount?: number }) => ({
id: p.id || uuidv4(), // Use existing ID or generate new one
pattern: p.pattern,
confidence: p.confidence,
successRate: (p.metadata?.success_rate as number) ?? 0.5,
usageCount: p.usageCount || 0,
contexts: (p.metadata?.contexts as string[]) ?? [],
createdAt: p.metadata?.created_at ? new Date(p.metadata.created_at as string | number) : new Date(),
lastUsedAt: p.metadata?.last_used_at ? new Date(p.metadata.last_used_at as string | number) : new Date()
}));
} catch (error) {
this.logger.warn('Failed to query patterns:', error);
return [];
}
}
/**
* Search for similar patterns using HNSW vector similarity
* Phase 0 M0.3: O(log n) complexity with <1ms p95 latency
*
* @param query Search query (text to find similar patterns for)
* @param k Number of results to return (default: 5)
* @returns Similar patterns with similarity scores, or empty array if HNSW not enabled
*/
async searchSimilarPatterns(query: string, k: number = 5): Promise<PatternSearchResult[]> {
if (!this.hnswAdapter) {
this.logger.debug('HNSW not enabled, searchSimilarPatterns returning empty');
return [];
}
try {
return await this.hnswAdapter.searchSimilar(query, k);
} catch (error) {
this.logger.warn('HNSW similarity search failed:', error);
return [];
}
}
/**
* Check if HNSW vector search is enabled
*/
isHNSWEnabled(): boolean {
return !!this.hnswAdapter;
}
/**
* Get HNSW adapter statistics
*/
async getHNSWStats(): Promise<{ patternCount: number; useRuvLLM: boolean; embeddingDimension: number } | null> {
if (!this.hnswAdapter) {
return null;
}
return this.hnswAdapter.getStats();
}
/**
* Get failure patterns
*/
getFailurePatterns(): FailurePattern[] {
return Array.from(this.failurePatterns.values())
.sort((a, b) => b.frequency - a.frequency);
}
/**
* Extract features from state for learning
*/
private extractFeatures(state: TaskState): number[] {
return [
state.taskComplexity,
state.requiredCapabilities.length / 10, // normalize
state.previousAttempts / 5, // normalize
state.availableResources,
state.timeConstraint ? Math.min(state.timeConstraint / 300000, 1) : 1 // normalize to 5 min
];
}
/**
* Encode state to string key for Q-table
*/
private encodeState(state: TaskState): string {
const features = this.extractFeatures(state);
return features.map(f => Math.round(f * 10) / 10).join(',');
}
/**
* Encode action to string key
*/
private encodeAction(action: AgentAction): string {
return `${action.strategy}:${action.parallelization.toFixed(1)}:${action.retryPolicy}`;
}
/**
* Extract experience from task execution
*/
private extractExperience(
task: Record<string, unknown>,
result: Record<string, unknown>,
feedback?: LearningFeedback
): TaskExperience {
const requirements = task.requirements as Record<string, unknown> | undefined;
const state: TaskState = {
taskComplexity: this.estimateComplexity(task),
requiredCapabilities: (requirements?.capabilities as string[]) || [],
contextFeatures: (task.context as Record<string, unknown>) || {},
previousAttempts: (task.previousAttempts as number) || 0,
availableResources: this.stateExtractor.extractState(task as unknown as QETask).availableResources,
timeConstraint: task.timeout as number | undefined
};
const action: AgentAction = {
strategy: (result.strategy as string) || 'default',
toolsUsed: (result.toolsUsed as string[]) || [],
parallelization: (result.parallelization as number) || 0.5,
retryPolicy: (result.retryPolicy as string) || 'exponential',
resourceAllocation: (result.resourceAllocation as number) || 0.5
};
// Next state (after execution)
const nextState: TaskState = {
...state,
previousAttempts: state.previousAttempts + 1,
availableResources: state.availableResources * 0.9 // resource consumption
};
return {
taskId: (task.id as string) || uuidv4(),
taskType: task.type as string,
state,
action,
reward: 0, // will be calculated
nextState,
timestamp: new Date(),
agentId: this.agentId
};
}
/**
* Calculate reward from execution result and feedback
*/
private calculateReward(result: Record<string, unknown>, feedback?: LearningFeedback): number {
let reward = 0;
// Success/failure (primary component)
reward += result.success ? 1.0 : -1.0;
// Execution time (faster is better)
const executionTime = result.executionTime as number | undefined;
if (executionTime) {
const timeFactor = Math.max(0, 1 - executionTime / 30000); // 30 sec baseline
reward += timeFactor * 0.5;
}
// Error rate penalty
const errors = result.errors as unknown[] | undefined;
if (errors) {
reward -= errors.length * 0.1;
}
// User feedback
if (feedback) {
reward += (feedback.rating - 0.5) * 2; // -1 to +1
reward -= feedback.issues.length * 0.2;
}
// Coverage/quality bonus (for test generation)
const coverage = result.coverage as number | undefined;
if (coverage) {
reward += (coverage - 0.8) * 2; // bonus above 80%
}
return Math.max(-2, Math.min(2, reward)); // clamp to [-2, 2]
}
/**
* Update Q-table with new experience (Q-learning algorithm)
*/
private async updateQTable(experience: TaskExperience): Promise<void> {
const stateKey = this.encodeState(experience.state);
const actionKey = this.encodeAction(experience.action);
const nextStateKey = this.encodeState(experience.nextState);
// Get or create state-action map
if (!this.qTable.has(stateKey)) {
this.qTable.set(stateKey, new Map());
}
const stateActions = this.qTable.get(stateKey)!;
// Get current Q-value
const currentQ = stateActions.get(actionKey) || 0;
// Get max Q-value for next state
const nextStateActions = this.qTable.get(nextStateKey) || new Map();
const maxNextQ = nextStateActions.size > 0
? Math.max(...Array.from(nextStateActions.values()))
: 0;
// Q-learning update: Q(s,a) = Q(s,a) + α * [r + γ * max(Q(s',a')) - Q(s,a)]
const newQ = currentQ + this.config.learningRate * (
experience.reward + this.config.discountFactor * maxNextQ - currentQ
);
stateActions.set(actionKey, newQ);
}
/**
* Perform batch update on experiences
*/
private async performBatchUpdate(): Promise<void> {
if (this.experiences.length < this.config.batchSize) {
return;
}
// Sample recent experiences
const batch = this.experiences.slice(-this.config.batchSize);
// Re-train on batch
for (const experience of batch) {
await this.updateQTable(experience);
}
this.logger.info(`Performed batch update on ${batch.length} experiences`);
}
/**
* Update learned patterns
* Now uses memoryStore persistence with agent filtering instead of in-memory Map
*/
private async updatePatterns(experience: TaskExperience): Promise<void> {
const patternKey = `${experience.taskType}:${experience.action.strategy}`;
try {
// Try to query existing pattern from memoryStore (agent-specific)
let existingPattern: LearnedPattern | null = null;
if (this.memoryStore && typeof this.memoryStore.queryPatternsByAgent === 'function') {
const agentPatterns = await this.memoryStore.queryPatternsByAgent(this.agentId, 0);
const found = agentPatterns.find((p: { pattern?: string }) => p.pattern === patternKey);
if (found && found.metadata) {
const metadata = typeof found.metadata === 'string'
? JSON.parse(found.metadata)
: found.metadata;
existingPattern = {
id: found.id || uuidv4(),
pattern: found.pattern,
confidence: found.confidence,
successRate: metadata.success_rate || 0.5,
usageCount: found.usageCount,
contexts: metadata.contexts || [experience.taskType],
createdAt: metadata.created_at ? new Date(metadata.created_at) : new Date(),
lastUsedAt: metadata.last_used_at ? new Date(metadata.last_used_at) : new Date()
};
}
}
if (existingPattern) {
// Update existing pattern
existingPattern.usageCount++;
existingPattern.lastUsedAt = new Date();
// Update success rate
if (experience.reward > 0) {
existingPattern.successRate = (existingPattern.successRate * (existingPattern.usageCount - 1) + 1) / existingPattern.usageCount;
} else {
existingPattern.successRate = (existingPattern.successRate * (existingPattern.usageCount - 1)) / existingPattern.usageCount;
}
// Update confidence
existingPattern.confidence = Math.min(0.95, existingPattern.confidence + 0.01);
// Persist pattern update to database
await this.memoryStore.storePattern({
id: existingPattern.id,
pattern: existingPattern.pattern,
confidence: existingPattern.confidence,
usageCount: existingPattern.usageCount,
metadata: {
agent_id: this.agentId,
success_rate: existingPattern.successRate,
contexts: existingPattern.contexts,
created_at: existingPattern.createdAt,
last_used_at: existingPattern.lastUsedAt
}
});
// Phase 0 M0.3: Update in HNSW for vector similarity search
if (this.hnswAdapter) {
try {
await this.hnswAdapter.storePattern({
...existingPattern,
agentId: this.agentId,
averageReward: experience.reward,
});
} catch (hnswError) {
this.logger.warn('HNSW pattern update failed, continuing with SQLite only:', hnswError);
}
}
} else {
// Create new pattern
const pattern: LearnedPattern = {
id: uuidv4(),
pattern: patternKey,
confidence: 0.5,
successRate: experience.reward > 0 ? 1.0 : 0.0,
usageCount: 1,
contexts: [experience.taskType],
createdAt: new Date(),
lastUsedAt: new Date()
};
// Persist new pattern to database
await this.memoryStore.storePattern({
id: pattern.id,
pattern: pattern.pattern,
confidence: pattern.confidence,
usageCount: pattern.usageCount,
metadata: {
agent_id: this.agentId,
success_rate: pattern.successRate,
contexts: pattern.contexts,
created_at: pattern.createdAt,
last_used_at: pattern.lastUsedAt
}
});
// Phase 0 M0.3: Also store in HNSW for vector similarity search
if (this.hnswAdapter) {
try {
await this.hnswAdapter.storePattern({
...pattern,
agentId: this.agentId,
averageReward: experience.reward,
});
} catch (hnswError) {
this.logger.warn('HNSW pattern storage failed, continuing with SQLite only:', hnswError);
}
}
// Emit pattern discovered event
await this.emitLearningEvent('pattern_discovered', pattern);
}
} catch (error) {
this.logger.warn('Failed to update patterns:', error);
// Continue execution even if pattern update fails
}
}
/**
* Detect failure patterns
*/
private async detectFailurePattern(experience: TaskExperience): Promise<void> {
const patternKey = `${experience.taskType}:failure`;
if (this.failurePatterns.has(patternKey)) {
const pattern = this.failurePatterns.get(patternKey)!;
pattern.frequency++;
pattern.confidence = Math.min(0.95, pattern.frequency / this.experiences.length);
} else {
const pattern: FailurePattern = {
id: uuidv4(),
pattern: patternKey,
frequency: 1,
contexts: [experience.taskType],
confidence: 0.1,
identifiedAt: new Date()
};
this.failurePatterns.set(patternKey, pattern);
}
}
/**
* Calculate improvement over time
*/
private async calculateImprovement(): Promise<LearningOutcome> {
if (this.experiences.length < 10) {
return this.createOutcome(false, 0, 0);
}
// Compare recent performance vs baseline
const recentExperiences = this.experiences.slice(-20);
const baselineExperiences = this.experiences.slice(0, 20);
const recentAvgReward = recentExperiences.reduce((sum, e) => sum + e.reward, 0) / recentExperiences.length;
const baselineAvgReward = baselineExperiences.reduce((sum, e) => sum + e.reward, 0) / baselineExperiences.length;
const improvementRate = baselineAvgReward !== 0
? ((recentAvgReward - baselineAvgReward) / Math.abs(baselineAvgReward)) * 100
: 0;
return this.createOutcome(
improvementRate > 0,
baselineAvgReward,
recentAvgReward,
improvementRate
);
}
/**
* Create learning outcome
*/
private async createOutcome(
improved: boolean,
previous: number,
current: number,
rate: number = 0
): Promise<LearningOutcome> {
const patterns = await this.getPatterns();
return {
improved,
previousPerformance: previous,
newPerformance: current,
improvementRate: rate,
confidence: Math.min(0.95, this.experiences.length / 100),
patterns: patterns.slice(0, 5),
timestamp: new Date()
};
}
/**
* Estimate task complexity
*/
private estimateComplexity(task: Record<string, unknown>): number {
let complexity = 0.5; // baseline
const requirements = task.requirements as Record<string, unknown> | undefined;
const capabilities = requirements?.capabilities as unknown[] | undefined;
if (capabilities) {
complexity += capabilities.length * 0.1;
}
const previousAttempts = task.previousAttempts as number | undefined;
if (previousAttempts) {
complexity += previousAttempts * 0.1;
}
return Math.min(1.0, complexity);
}
/**
* Convert Q-value to confidence score
*/
private valueToConfidence(value: number): number {
return Math.max(0, Math.min(1, (value + 2) / 4)); // map [-2, 2] to [0, 1]
}
/**
* Decay exploration rate
*/
private decayExploration(): void {
this.config.explorationRate = Math.max(
this.config.minExplorationRate,
this.config.explorationRate * this.config.explorationDecay
);
}
/**
* Save learning state to memory
*/
private async saveState(): Promise<void> {
const patterns = await this.getPatterns();
const state: LearningModelState = {
agentId: this.agentId,
qTable: this.serializeQTable(),
experiences: this.experiences.slice(-1000), // keep last 1000
patterns,
config: this.config,
performance: await this.getCurrentPerformance(),
version: PACKAGE_VERSION,
lastUpdated: new Date(),
size: await this.calculateStateSize()
};
// Check size limit
if (state.size > this.config.maxMemorySize) {
this.logger.warn(`Learning state exceeds max size (${state.size} bytes), pruning...`);
// Fix: Prune in-memory array, not just saved state
this.experiences = this.experiences.slice(-500);
state.experiences = this.experiences;
state.size = await this.calculateStateSize();
}
// Serialize state to Record<string, unknown> for type safety
// Convert Date objects to ISO strings for JSON serialization
const serializableState: Record<string, unknown> = {
...state,
lastUpdated: state.lastUpdated.toISOString(),
experiences: state.experiences.map(exp => ({
...exp,
timestamp: exp.timestamp.toISOString()
})),
patterns: state.patterns.map(p => ({
...p,
createdAt: p.createdAt.toISOString(),
lastUsedAt: p.lastUsedAt.toISOString()
}))
};
await this.memoryStore.store(
`phase2/learning/${this.agentId}/state`,
serializableState,
{ partition: 'learning' }
);
this.logger.info(`Saved learning state (${state.size} bytes, ${state.experiences.length} experiences)`);
}
/**
* Load learning state from memory
* Pattern Map removed - patterns now persisted directly via memoryStore.storePattern()
*/
private async loadState(): Promise<void> {
try {
const state = await this.memoryStore.retrieve(
`phase2/learning/${this.agentId}/state`,
{ partition: 'learning' }
) as LearningModelState | null;
if (state) {
this.deserializeQTable(state.qTable);
this.experiences = state.experiences;
// REMOVED: this.patterns = new Map(state.patterns.map(p => [p.pattern, p]));
// Patterns are now persisted via memoryStore.storePattern() and queried via queryPatternsByConfidence()
this.taskCount = state.experiences.length;
this.logger.info(`Loaded learning state: ${state.experiences.length} experiences`);
}
} catch (error) {
this.logger.warn('No previous learning state found, starting fresh');
}
}
/**
* Serialize Q-table for storage
*/
private serializeQTable(): Record<string, Record<string, number>> {
const serialized: Record<string, Record<string, number>> = {};
for (const [state, actions] of this.qTable.entries()) {
serialized[state] = Object.fromEntries(actions.entries());
}
return serialized;
}
/**
* Deserialize Q-table from storage
*/
private deserializeQTable(data: Record<string, Record<string, number>>): void {
this.qTable.clear();
for (const [state, actions] of Object.entries(data)) {
this.qTable.set(state, new Map(Object.entries(actions)));
}
}
/**
* Serialize Q-table for QLearning import (converts to QValue format)
*/
private serializeQTableForQLearning(): Record<string, Record<string, { state: string; action: string; value: number; updateCount: number; lastUpdated: number }>> {
const serialized: Record<string, Record<string, { state: string; action: string; value: number; updateCount: number; lastUpdated: number }>> = {};
for (const [state, actions] of this.qTable.entries()) {
serialized[state] = {};
for (const [action, value] of actions.entries()) {
serialized[state][action] = {
state,
action,
value,
updateCount: 1,
lastUpdated: Date.now()
};
}
}
return serialized;
}
/**
* Deserialize Q-table from QLearning export (extracts values from QValue format)
*/
private deserializeQTableFromQLearning(data: Record<string, Record<string, { value: number }>>): void {
this.qTable.clear();
for (const [state, actions] of Object.entries(data)) {
const actionMap = new Map<string, number>();
for (const [action, qValue] of Object.entries(actions)) {
actionMap.set(action, qValue.value);
}
this.qTable.set(state, actionMap);
}
}
/**
* Calculate state size in bytes
*/
private async calculateStateSize(): Promise<number> {
const patterns = await this.getPatterns();
return JSON.stringify({
qTable: this.serializeQTable(),
experiences: this.experiences,
patterns
}).length;
}
/**
* Get current performance metrics
*/
private async getCurrentPerformance(): Promise<PerformanceMetrics> {
const recentExperiences = this.experiences.slice(-100);
const now = new Date();
const periodStart = new Date(now.getTime() - 24 * 60 * 60 * 1000); // last 24 hours
const avgReward = recentExperiences.length > 0
? recentExperiences.reduce((sum, e) => sum + e.reward, 0) / recentExperiences.length
: 0;
const successRate = recentExperiences.length > 0
? recentExperiences.filter(e => e.reward > 0).length / recentExperiences.length
: 0;
return {
agentId: this.agentId,
period: { start: periodStart, end: now },
metrics: {
tasksCompleted: recentExperiences.length,
successRate,
averageExecutionTime: 0, // Not tracked at this level
errorRate: 1 - successRate,
userSatisfaction: successRate, // Use success rate as proxy
resourceEfficiency: avgReward > 0 ? Math.min(1, avgReward) : 0
},
trends: []
};
}
/**
* Emit learning event
*/
private async emitLearningEvent(type: string, data: unknown): Promise<void> {
const event: LearningEvent = {
id: uuidv4(),
type: type as LearningEvent['type'],
agentId: this.agentId,
data,
timestamp: new Date()
};
// Serialize event to Record<string, unknown> for type safety
const serializableEvent: Record<string, unknown> = {
...event,
timestamp: event.timestamp.toISOString()
};
await this.memoryStore.storeEvent({
type: `learning:${type}`,
payload: serializableEvent,
source: this.agentId,
timestamp: Date.now()
});
}
/**
* Get current exploration rate
*/
getExplorationRate(): number {
return this.config.explorationRate;
}
/**
* Get total experiences
*/
getTotalExperiences(): number {
return this.experiences.length;
}
/**
* Enable/disable learning
*/
setEnabled(enabled: boolean): void {
this.config.enabled = enabled;
}
/**
* Check if learning is enabled
*/
isEnabled(): boolean {
return this.config.enabled;
}
/**
* Enable Q-learning mode (Phase 2 Integration)
* Switches from basic Q-table to full QLearning algorithm with experience replay
*/
enableQLearning(config?: Partial<QLearningConfig>): void {
const qLearningConfig: Partial<QLearningConfig> = {
learningRate: this.config.learningRate,
discountFactor: this.config.discountFactor,
explorationRate: this.config.explorationRate,
explorationDecay: this.config.explorationDecay,
minExplorationRate: this.config.minExplorationRate,
useExperienceReplay: true,
replayBufferSize: 10000,
batchSize: this.config.batchSize,
...config
};
this.qLearning = new QLearning(qLearningConfig);
this.useQLearning = true;
// Import existing Q-table into QLearning if we have data
if (this.qTable.size > 0) {
const serialized = this.serializeQTableForQLearning();
this.qLearning.import({
qTable: serialized,
config: qLearningConfig as QLearningConfig,
stepCount: this.taskCount,
episodeCount: Math.floor(this.taskCount / 10)
});
}
this.logger.info(`Q-learning mode enabled for agent ${this.agentId}`, {
config: qLearningConfig
});
}
/**
* Disable Q-learning mode (revert to basic implementation)
*/
disableQLearning(): void {
if (this.qLearning && this.useQLearning) {
// Export Q-learning state to basic Q-table
const exported = this.qLearning.export();
this.deserializeQTableFromQLearning(exported.qTable);
}
this.qLearning = undefined;
this.useQLearning = false;
this.logger.info(`Q-learning mode disabled for agent ${this.agentId}`);
}
/**
* Learn from experience using Q-learning (when enabled)
* This method integrates with the QLearning algorithm
*/
async learnFromExperience(experience: TaskExperience): Promise<void> {
if (!this.config.enabled) {
return;
}
if (this.useQLearning && this.qLearning) {
// Use QLearning algorithm
this.qLearning.update(experience);
this.experiences.push(experience);
// Perform batch update periodically
if (this.taskCount % this.config.updateFrequency === 0) {
this.qLearning.batchUpdate();
}
// End episode periodically to trigger exploration decay
if (this.taskCount % 10 === 0) {
this.qLearning.endEpisode();
}
} else {
// Use legacy Q-table implementation
await this.updateQTable(experience);
this.experiences.push(experience);
}
this.taskCount++;
// Fix: Decay exploration rate after each experience
this.decayExploration();
}
/**
* Select action with policy (Q-learning integration)
* Uses epsilon-greedy policy when Q-learning is enabled
*/
async selectActionWithPolicy(state: TaskState, availableActions: AgentAction[]): Promise<AgentAction> {
if (this.useQLearning && this.qLearning) {
// Use Q-learning's epsilon-greedy policy
return this.qLearning.selectAction(state, availableActions);
}
// Fallback to recommendation-based selection
const recommendation = await this.recommendStrategy(state);
// Find the action matching the recommended strategy
const matchingAction = availableActions.find(
action => action.strategy === recommendation.strategy
);
return matchingAction || availableActions[0];
}
/**
* Get Q-learning statistics (when enabled)
*/
getQLearningStats(): {
enabled: boolean;
stats?: {
steps: number;
episodes: number;
tableSize: number;
explorationRate: number;
avgQValue: number;
maxQValue: number;
minQValue: number;
};
} {
if (!this.useQLearning || !this.qLearning) {
return { enabled: false };
}
return {
enabled: true,
stats: this.qLearning.getStatistics()
};
}
/**
* Check if Q-learning mode is enabled
*/
isQLearningEnabled(): boolean {
return this.useQLearning;
}
/**
* Cleanup resources (timers, connections)
* Call this before destroying the LearningEngine instance
*
* Architecture: No cleanup needed - LearningEngine no longer manages Database instances.
* The shared SwarmMemoryManager handles database lifecycle management.
*/
dispose(): void {
// Clean up experience sharing listeners
if (this.experienceSharing) {
this.experienceSharing.removeAllListeners('experience_received');
this.experienceSharing = undefined;
}
this.logger.debug(`LearningEngine disposed for agent ${this.agentId}`);
}
/**
* Set the RL algorithm to use (Q-Learning, SARSA, etc.)
* Supports dynamic algorithm switching with state transfer
*/
setAlgorithm(algorithm: RLAlgorithmType): void {
if (algorithm === 'legacy') {
// Revert to legacy Q-table implementation
this.disableRLAlgorithm();
return;
}
const rlConfig = {
learningRate: this.config.learningRate,
discountFactor: this.config.discountFactor,
explorationRate: this.config.explorationRate,
explorationDecay: this.config.explorationDecay,
minExplorationRate: this.config.minExplorationRate,
useExperienceReplay: true,
replayBufferSize: 10000,
batchSize: this.config.batchSize
};
// Export current algorithm state if exists
let existingState;
if (this.rlAlgorithm) {
existingState = this.rlAlgorithm.export();
} else if (this.qLearning) {
// Migrate from legacy qLearning
existingState = this.qLearning.export();
}
// Create new algorithm instance
switch (algorithm) {
case 'q-learning':
this.rlAlgorithm = new QLearning(rlConfig);
this.qLearning = this.rlAlgorithm as QLearning; // Backward compatibility
this.useQLearning = true;
break;
case 'sarsa':
this.rlAlgorithm = new SARSALearner(rlConfig);
this.useQLearning = false; // SARSA is different from Q-Learning
break;
case 'actor-critic':
this.rlAlgorithm = new ActorCriticLearner({
...rlConfig,
actorLearningRate: rlConfig.learningRate * 0.1,
criticLearningRate: rlConfig.learningRate,
entropyCoefficient: 0.01,
temperature: 1.0,
normalizeAdvantage: true,
targetUpdateFrequency: 100
});
this.useQLearning = false;
break;
case 'ppo':
this.rlAlgorithm = new PPOLearner({
...rlConfig,
clipEpsilon: 0.2,
ppoEpochs: 4,
miniBatchSize: 32,
valueLossCoefficient: 0.5,
entropyCoefficient: 0.01,
gaeLambda: 0.95,
maxGradNorm: 0.5,
clipValueLoss: true,
policyLearningRate: rlConfig.learningRate,
valueLearningRate: rlConfig.learningRate * 3
});
this.useQLearning = false;
break;
default:
throw new Error(`Unknown RL algorithm: ${algorithm}`);
}
// Import existing state if available
if (existingState) {
this.rlAlgorithm.import(existingState);
this.logger.info(`Migrated existing Q-table to ${algorithm} (${this.rlAlgorithm.getTableSize()} state-action pairs)`);
} else if (this.qTable.size > 0) {
// Import from legacy Q-table
this.importLegacyQTable();
}
this.config.algorithm = algorithm;
this.logger.info(`Switched to ${algorithm} algorithm for agent ${this.agentId}`, { config: rlConfig });
}
/**
* Disable RL algorithm and revert to legacy Q-table
*/
private disableRLAlgorithm(): void {
if (this.rlAlgorithm) {
// Export RL algorithm state to legacy Q-table
const exported = this.rlAlgorithm.export();
this.deserializeQTableFromQLearning(exported.qTable);
} else if (this.qLearning && this.useQLearning) {
// Export from legacy qLearning
const exported = this.qLearning.export();
this.deserializeQTableFromQLearning(exported.qTable);
}
this.rlAlgorithm = undefined;
this.qLearning = undefined;
this.useQLearning = false;
this.config.algorithm = 'legacy';
this.logger.info(`Disabled RL algorithm for agent ${this.agentId}, reverted to legacy Q-table`);
}
/**
* Import legacy Q-table into current RL algorithm
*/
private importLegacyQTable(): void {
if (!this.rlAlgorithm) {
return;
}
const serialized = this.serializeQTableForQLearning();
this.rlAlgorithm.import({
qTable: serialized,
config: {
learningRate: this.config.learningRate,
discountFactor: this.config.discountFactor,
explorationRate: this.config.explorationRate,
explorationDecay: this.config.explorationDecay,
minExplorationRate: this.config.minExplorationRate,
useExperienceReplay: true,
replayBufferSize: 10000,
batchSize: this.config.batchSize
},
stepCount: this.taskCount,
episodeCount: Math.floor(this.taskCount / 10)
});
this.logger.info(`Imported legacy Q-table to ${this.config.algorithm} (${this.rlAlgorithm.getTableSize()} state-action pairs)`);
}
/**
* Get current RL algorithm type
*/
getAlgorithm(): RLAlgorithmType {
return this.config.algorithm ?? 'legacy';
}
/**
* Get RL algorithm instance (for advanced use)
*/
getRLAlgorithm(): AbstractRLLearner | undefined {
return this.rlAlgorithm;
}
/**
* Get algorithm-specific statistics
*/
getAlgorithmStats(): {
algorithm: RLAlgorithmType;
stats?: ReturnType<AbstractRLLearner['getStatistics']>;
} {
if (this.rlAlgorithm) {
return {
algorithm: this.config.algorithm ?? 'legacy',
stats: this.rlAlgorithm.getStatistics()
};
}
return {
algorithm: 'legacy'
};
}
// ============================================================================
// Experience Sharing Integration
// ============================================================================
/**
* Enable experience sharing with other agents
* Connects this LearningEngine to an ExperienceSharingProtocol
*
* @param protocol - The ExperienceSharingProtocol instance to use
*/
enableExperienceSharing(protocol: ExperienceSharingProtocol): void {
this.experienceSharing = protocol;
this.config.enableExperienceSharing = true;
// Subscribe to incoming experiences from other agents
protocol.on('experience_received', async (event: { experienceId: string; sourceAgentId: string }) => {
await this.handleReceivedExperience(event.experienceId);
});
this.logger.info(`Experience sharing enabled for agent ${this.agentId}`);
}
/**
* Disable experience sharing
*/
disableExperienceSharing(): void {
if (this.experienceSharing) {
this.experienceSharing.removeAllListeners('experience_received');
this.experienceSharing = undefined;
}
this.config.enableExperienceSharing = false;
this.logger.info(`Experience sharing disabled for agent ${this.agentId}`);
}
/**
* Check if experience sharing is enabled
*/
isExperienceSharingEnabled(): boolean {
return this.config.enableExperienceSharing === true && this.experienceSharing !== undefined;
}
/**
* Get the ExperienceSharingProtocol instance (if enabled)
*/
getExperienceSharingProtocol(): ExperienceSharingProtocol | undefined {
return this.experienceSharing;
}
/**
* Share an experience with other agents
* Called internally after learning from execution when sharing is enabled
*
* @param experience - The experience to share
* @param priority - Optional priority override (0-1, higher = more valuable)
*/
async shareExperienceWithPeers(experience: TaskExperience, priority?: number): Promise<string | undefined> {
if (!this.experienceSharing) {
return undefined;
}
// Calculate priority from reward if not provided
const calculatedPriority = priority ?? this.calculateExperiencePriority(experience);
// Only share if priority meets threshold
if (calculatedPriority < (this.config.experienceSharingPriority ?? 0.5)) {
this.logger.debug(`Experience below sharing threshold: ${calculatedPriority} < ${this.config.experienceSharingPriority}`);
return undefined;
}
try {
const experienceId = await this.experienceSharing.shareExperience(experience, calculatedPriority);
this.logger.debug(`Shared experience with peers: ${experienceId}`, { priority: calculatedPriority });
return experienceId;
} catch (error) {
this.logger.warn('Failed to share experience with peers:', error);
return undefined;
}
}
/**
* Calculate the priority/value of an experience for sharing
* Higher rewards and successful experiences get higher priority
*/
private calculateExperiencePriority(experience: TaskExperience): number {
// Base priority from reward (normalized from [-2, 2] to [0, 1])
const rewardPriority = (experience.reward + 2) / 4;
// Boost for successful experiences (positive reward)
const successBoost = experience.reward > 0 ? 0.2 : 0;
// Boost for unusual/interesting states (high complexity)
const complexityBoost = experience.state.taskComplexity > 0.7 ? 0.1 : 0;
return Math.min(1.0, Math.max(0, rewardPriority + successBoost + complexityBoost));
}
/**
* Handle an experience received from another agent
* Integrates the shared experience into local learning
*/
private async handleReceivedExperience(experienceId: string): Promise<void> {
if (!this.experienceSharing) {
return;
}
try {
// Get the shared experience from the protocol
const sharedExperiences = await this.experienceSharing.getRelevantExperiences(
{ taskComplexity: 0, requiredCapabilities: [], contextFeatures: {}, previousAttempts: 0, availableResources: 1 },
100
);
const sharedExp = sharedExperiences.find(e => e.id === experienceId);
if (!sharedExp) {
return; // Experience not found or already processed
}
// Learn from the shared experience (with reduced weight)
// Apply a transfer learning discount factor
const transferDiscount = 0.5; // Shared experiences weighted 50%
const adjustedExperience = {
...sharedExp.experience,
reward: sharedExp.experience.reward * transferDiscount
};
// Update Q-table with shared experience
await this.updateQTable(adjustedExperience);
// Store in local experiences buffer
this.experiences.push(adjustedExperience);
this.logger.debug(`Integrated shared experience: ${experienceId}`, {
from: sharedExp.sourceAgentId,
originalReward: sharedExp.experience.reward,
adjustedReward: adjustedExperience.reward
});
} catch (error) {
this.logger.warn(`Failed to handle received experience ${experienceId}:`, error);
}
}
/**
* Query relevant experiences from peers for a given state
* Useful for transfer learning before executing a task
*
* @param state - The current task state
* @param limit - Maximum number of experiences to retrieve
*/
async queryPeerExperiences(state: TaskState, limit: number = 10): Promise<TaskExperience[]> {
if (!this.experienceSharing) {
return [];
}
try {
const sharedExperiences = await this.experienceSharing.getRelevantExperiences(state, limit);
return sharedExperiences.map(se => se.experience);
} catch (error) {
this.logger.warn('Failed to query peer experiences:', error);
return [];
}
}
/**
* Get experience sharing statistics
*/
getExperienceSharingStats(): {
enabled: boolean;
stats?: {
experiencesShared: number;
experiencesReceived: number;
activeConnections: number;
};
} {
if (!this.experienceSharing) {
return { enabled: false };
}
const stats = this.experienceSharing.getStats();
return {
enabled: true,
stats: {
experiencesShared: stats.experiencesShared,
experiencesReceived: stats.experiencesReceived,
activeConnections: stats.activeConnections
}
};
}
}