@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
875 lines • 132 kB
JavaScript
/**
* Agent element implementation.
* Autonomous goal-oriented actors with decision-making capabilities.
*
* SECURITY MEASURES IMPLEMENTED:
* 1. Goal validation to prevent malicious objectives
* 2. Decision framework sandboxing
* 3. State size limits to prevent DoS
* 4. Risk assessment for damage prevention
* 5. Audit logging for all decisions and actions
*/
import { BaseElement } from '../BaseElement.js';
import { ElementType } from '../../portfolio/types.js';
import { randomBytes } from 'node:crypto';
import { sanitizeInput } from '../../security/InputValidator.js';
import { UnicodeValidator } from '../../security/validators/unicodeValidator.js';
import { SecurityMonitor } from '../../security/securityMonitor.js';
import { logger } from '../../utils/logger.js';
import { ErrorHandler, ErrorCategory } from '../../utils/ErrorHandler.js';
import { EvictingQueue } from '../../utils/EvictingQueue.js';
import { ValidationErrorCodes } from '../../utils/errorCodes.js';
import { AGENT_LIMITS, AGENT_DEFAULTS, AGENT_THRESHOLDS, DECISION_FRAMEWORKS, RISK_TOLERANCE_LEVELS, COMMIT_PERSISTED_VERSION } from './constants.js';
import { validateRuleEngineConfig } from './ruleEngineConfig.js';
import { applyGoalTemplate, calculateEisenhowerQuadrant, recommendGoalTemplate, validateGoalAgainstTemplate } from './goalTemplates.js';
export class Agent extends BaseElement {
// instructions and content inherited from BaseElement (v2.0 dual-field architecture)
state;
isDirtyState = false;
ruleEngineConfig;
_decisionHistory;
constructor(metadata, metadataService) {
// Sanitize all inputs
const sanitizedMetadata = {
...metadata,
name: metadata.name ? sanitizeInput(UnicodeValidator.normalize(metadata.name).normalizedContent, 100) : undefined,
description: metadata.description ? sanitizeInput(UnicodeValidator.normalize(metadata.description).normalizedContent, 500) : undefined,
specializations: metadata.specializations?.map(s => sanitizeInput(s, 50)),
decisionFramework: metadata.decisionFramework || AGENT_DEFAULTS.DECISION_FRAMEWORK,
riskTolerance: metadata.riskTolerance || AGENT_DEFAULTS.RISK_TOLERANCE,
learningEnabled: metadata.learningEnabled ?? AGENT_DEFAULTS.LEARNING_ENABLED,
maxConcurrentGoals: metadata.maxConcurrentGoals ?? AGENT_DEFAULTS.MAX_CONCURRENT_GOALS
};
// MEDIUM PRIORITY IMPROVEMENT: Validate decision framework configuration
// Ensures only supported frameworks are used
if (sanitizedMetadata.decisionFramework &&
!DECISION_FRAMEWORKS.includes(sanitizedMetadata.decisionFramework)) {
throw ErrorHandler.createError(`Invalid decision framework: ${sanitizedMetadata.decisionFramework}. ` +
`Supported frameworks: ${DECISION_FRAMEWORKS.join(', ')}`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_FRAMEWORK);
}
// Validate risk tolerance level
if (sanitizedMetadata.riskTolerance &&
!RISK_TOLERANCE_LEVELS.includes(sanitizedMetadata.riskTolerance)) {
throw ErrorHandler.createError(`Invalid risk tolerance: ${sanitizedMetadata.riskTolerance}. ` +
`Supported levels: ${RISK_TOLERANCE_LEVELS.join(', ')}`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_RISK_TOLERANCE);
}
// Validate max concurrent goals
if (sanitizedMetadata.maxConcurrentGoals !== undefined) {
const maxGoals = sanitizedMetadata.maxConcurrentGoals;
if (!Number.isInteger(maxGoals) || maxGoals < 1 || maxGoals > AGENT_LIMITS.MAX_GOALS) {
throw ErrorHandler.createError(`maxConcurrentGoals must be between 1 and ${AGENT_LIMITS.MAX_GOALS}`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.INVALID_RANGE);
}
}
super(ElementType.AGENT, sanitizedMetadata, metadataService);
// Initialize state with version tracking for optimistic locking (Issue #24)
this.state = {
goals: [],
decisions: [],
context: {},
lastActive: new Date(),
sessionCount: 0,
stateVersion: 1 // Start at version 1
};
// Bounded FIFO queue for decision history
this._decisionHistory = new EvictingQueue(AGENT_LIMITS.MAX_DECISION_HISTORY);
// Set agent-specific extensions
this.extensions = {
decisionFramework: sanitizedMetadata.decisionFramework,
riskTolerance: sanitizedMetadata.riskTolerance,
learningEnabled: sanitizedMetadata.learningEnabled,
specializations: sanitizedMetadata.specializations || [],
ruleEngineConfig: metadata.ruleEngineConfig
};
// Initialize rule engine configuration (with validation)
this.ruleEngineConfig = validateRuleEngineConfig(this.extensions.ruleEngineConfig || {});
}
/**
* Add a new goal with security validation
*
* @param goal - Goal configuration
* @param options - Optional configuration for strict validation mode
* @returns The created goal with any security warnings attached
*
* @since v2.0.0 - Security validation is advisory by default (Issue #112)
*/
addGoal(goal, options) {
// Validate goal count
if (this.state.goals.length >= AGENT_LIMITS.MAX_GOALS) {
throw ErrorHandler.createError(`Maximum number of goals (${AGENT_LIMITS.MAX_GOALS}) reached`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.MAX_GOALS_EXCEEDED);
}
// Normalize and validate BEFORE sanitization (Issue #112)
const normalizedDescription = UnicodeValidator.normalize(goal.description || '').normalizedContent;
// Validate goal for security threats on ORIGINAL input before sanitization
// This ensures patterns like backticks, $, etc. are detected before being stripped
const securityCheck = this.validateGoalSecurity(normalizedDescription);
// Now sanitize for storage (removes shell metacharacters)
const sanitizedDescription = sanitizeInput(normalizedDescription, AGENT_LIMITS.MAX_GOAL_LENGTH);
if (!sanitizedDescription || sanitizedDescription.length < 3) {
throw ErrorHandler.createError('Goal description must be at least 3 characters', ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.GOAL_TOO_SHORT);
}
// Handle security validation results (advisory by default, blocking if strict mode enabled)
if (!securityCheck.safe) {
// Log security event for audit trail
SecurityMonitor.logSecurityEvent({
type: 'CONTENT_INJECTION_ATTEMPT',
severity: options?.strict ? 'HIGH' : 'MEDIUM',
source: 'Agent.addGoal',
details: `Goal with security warnings ${options?.strict ? 'rejected' : 'created'}: ${securityCheck.warnings?.join(', ')}`,
additionalData: { agentId: this.id, strict: options?.strict }
});
// In strict mode, throw error (backward compatible behavior)
if (options?.strict) {
throw ErrorHandler.createError(`Goal contains potentially harmful content: ${securityCheck.warnings?.join(', ')}`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.HARMFUL_CONTENT);
}
// Otherwise, continue with advisory warnings attached to goal
}
// Calculate Eisenhower quadrant
const importance = goal.importance || 5;
const urgency = goal.urgency || 5;
const eisenhowerQuadrant = calculateEisenhowerQuadrant(importance, urgency);
// Create new goal with security warnings (if any)
const newGoal = {
id: `goal_${Date.now()}_${randomBytes(6).toString('hex')}`,
description: sanitizedDescription,
priority: goal.priority || AGENT_DEFAULTS.GOAL_PRIORITY,
status: 'pending',
importance,
urgency,
eisenhowerQuadrant,
createdAt: new Date(),
updatedAt: new Date(),
dependencies: goal.dependencies || [],
riskLevel: goal.riskLevel || 'low',
estimatedEffort: goal.estimatedEffort,
notes: goal.notes ? sanitizeInput(goal.notes, 500) : undefined,
// Store security warnings for LLM review (advisory pattern)
securityWarnings: securityCheck.warnings && securityCheck.warnings.length > 0
? securityCheck.warnings
: undefined
};
// MEDIUM PRIORITY IMPROVEMENT: Detect dependency cycles before adding goal
if (newGoal.dependencies && newGoal.dependencies.length > 0) {
const cycleCheck = this.detectDependencyCycle(newGoal.id, newGoal.dependencies);
if (cycleCheck.hasCycle) {
throw ErrorHandler.createError(`Dependency cycle detected: ${cycleCheck.path.join(' → ')}`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.DEPENDENCY_CYCLE);
}
}
this.state.goals.push(newGoal);
// Note: stateVersion is incremented on successful save, not here (Issue #123 fix)
this.isDirtyState = true;
this.markDirty();
logger.info(`Goal added to agent ${this.metadata.name}`, { goalId: newGoal.id });
return newGoal;
}
/**
* Record a decision made by the LLM (or programmatic guardrail)
*
* This is NOT a decision-maker, it's a decision RECORDER.
* The LLM makes decisions, this method just persists them for audit trail.
*
* @since v2.0.0 - Agentic Loop Redesign
*/
recordDecision(decision) {
const goal = this.state.goals.find(g => g.id === decision.goalId);
if (!goal) {
throw ErrorHandler.createError(`Goal ${decision.goalId} not found`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.GOAL_NOT_FOUND);
}
// Create decision record
const decisionRecord = {
id: `decision_${Date.now()}_${randomBytes(6).toString('hex')}`,
goalId: decision.goalId,
timestamp: new Date(),
decision: sanitizeInput(decision.decision, 500),
reasoning: sanitizeInput(decision.reasoning, 1000),
framework: 'llm_driven', // v2.0 agents are LLM-driven
confidence: Math.max(0, Math.min(1, decision.confidence)),
riskAssessment: decision.riskAssessment || {
level: 'low',
score: 0,
factors: [],
mitigations: []
},
outcome: decision.outcome
};
// Bounded FIFO eviction — EvictingQueue handles capacity
this._decisionHistory.push(decisionRecord);
this.state.decisions = this._decisionHistory.toJSON();
// Note: stateVersion is incremented on successful save, not here (Issue #123 fix)
this.isDirtyState = true;
this.markDirty();
// Log for audit trail
SecurityMonitor.logSecurityEvent({
type: 'AGENT_DECISION',
severity: 'LOW',
source: 'Agent.recordDecision',
details: `Agent ${this.metadata.name} recorded decision for goal ${decision.goalId}`,
additionalData: {
agentId: this.id,
goalId: decision.goalId,
framework: 'llm_driven',
riskLevel: decisionRecord.riskAssessment.level
}
});
logger.info(`Decision recorded for agent ${this.metadata.name}`, {
goalId: decision.goalId,
decision: decision.decision
});
return decisionRecord;
}
/**
* Assess risk for a decision or action
*
* This is a CHECKLIST, not a decision maker. It flags known risk factors
* that the LLM should consider when making decisions.
*
* Similar to how GitHub flags "this file is large" or "this PR changes many files" -
* programmatic signals that inform semantic judgment.
*
* @since v2.0.0 - Refactored for LLM-first agentic loop
*/
assessRisk(action, goal, context) {
const factors = [];
let riskScore = 0;
// Factor: Immediate execution of high-risk goal
if (action === 'execute_immediately' && goal.riskLevel === 'high') {
factors.push('Immediate execution requested for high-risk goal');
riskScore += 30;
}
// Factor: Low confidence in decision
const confidence = context.decisionConfidence || 1.0;
if (confidence < 0.6) {
factors.push(`Low confidence in decision (${(confidence * 100).toFixed(0)}%)`);
riskScore += 20;
}
// Factor: Complex dependency chains
if (goal.dependencies && goal.dependencies.length > 3) {
factors.push(`Complex dependency chain (${goal.dependencies.length} dependencies)`);
riskScore += 15;
}
// Factor: Risk tolerance mismatch
if (this.extensions?.riskTolerance === 'aggressive' && goal.riskLevel === 'high') {
factors.push('Aggressive risk tolerance combined with high-risk goal');
riskScore += 25;
}
// Factor: Concurrent goals at limit
const activeGoals = this.state.goals.filter(g => g.status === 'in_progress').length;
const maxConcurrent = this.metadata.maxConcurrentGoals || AGENT_DEFAULTS.MAX_CONCURRENT_GOALS;
if (activeGoals >= maxConcurrent * AGENT_THRESHOLDS.CONCURRENT_GOAL_WARNING) {
factors.push(`High concurrent goal load (${activeGoals}/${maxConcurrent})`);
riskScore += 10;
}
// Map score to level
let level;
const mitigations = [];
if (riskScore >= 50) {
level = 'high';
mitigations.push('Request human approval before proceeding');
mitigations.push('Create backup/rollback plan');
mitigations.push('Enable detailed monitoring and logging');
mitigations.push('Consider breaking into smaller steps');
}
else if (riskScore >= 25) {
level = 'medium';
mitigations.push('Add progress checkpoints');
mitigations.push('Review results after completion');
mitigations.push('Monitor for unexpected outcomes');
}
else {
level = 'low';
mitigations.push('Standard monitoring sufficient');
}
return {
level,
score: riskScore, // Expose numeric score for transparency
factors,
mitigations
};
}
/**
* Evaluate programmatic constraints on a goal
*
* Returns blocking constraints that MUST be satisfied before proceeding.
* The LLM can see these and decide how to handle them.
*
* @since v2.0.0 - Extracted from ruleBasedDecision for LLM-first agentic loop
*/
evaluateConstraints(goal) {
const blockers = [];
const warnings = [];
// HARD CONSTRAINT: Incomplete dependencies
if (goal.dependencies && goal.dependencies.length > 0) {
const incompleteDeps = goal.dependencies.filter(depId => {
const dep = this.state.goals.find(g => g.id === depId);
return dep && dep.status !== 'completed';
});
if (incompleteDeps.length > 0) {
blockers.push(`${incompleteDeps.length} incomplete dependencies`);
}
}
// HARD CONSTRAINT: Max concurrent goals
const activeGoals = this.state.goals.filter(g => g.status === 'in_progress').length;
const maxConcurrent = this.metadata.maxConcurrentGoals || AGENT_DEFAULTS.MAX_CONCURRENT_GOALS;
if (activeGoals >= maxConcurrent) {
blockers.push(`Maximum concurrent goals reached (${maxConcurrent})`);
}
// SOFT CONSTRAINT: Risk + tolerance mismatch
if (goal.riskLevel === 'high' && this.extensions?.riskTolerance === 'conservative') {
warnings.push('High-risk goal with conservative risk tolerance - approval recommended');
}
return {
canProceed: blockers.length === 0,
blockers,
warnings
};
}
/**
* Calculate a programmatic "priority score" for a goal
*
* This is a simple heuristic the LLM can consider when prioritizing.
* The LLM is free to ignore this score if it has better judgment.
*
* @since v2.0.0 - Extracted from programmaticDecision for LLM-first agentic loop
*/
calculatePriorityScore(goal) {
let score = 0;
const factors = [];
const breakdown = {};
// Factor 1: Eisenhower matrix
if (goal.eisenhowerQuadrant === 'do_first') {
score += this.ruleEngineConfig.programmatic.scoreWeights.eisenhower.doFirst;
breakdown['eisenhower'] = this.ruleEngineConfig.programmatic.scoreWeights.eisenhower.doFirst;
factors.push('High importance and urgency (Do First quadrant)');
}
else if (goal.eisenhowerQuadrant === 'schedule') {
score += this.ruleEngineConfig.programmatic.scoreWeights.eisenhower.schedule;
breakdown['eisenhower'] = this.ruleEngineConfig.programmatic.scoreWeights.eisenhower.schedule;
factors.push('High importance, low urgency (Schedule quadrant)');
}
else if (goal.eisenhowerQuadrant === 'delegate') {
score += this.ruleEngineConfig.programmatic.scoreWeights.eisenhower.delegate;
breakdown['eisenhower'] = this.ruleEngineConfig.programmatic.scoreWeights.eisenhower.delegate;
factors.push('Low importance, high urgency (Delegate quadrant)');
}
// Factor 2: Risk level
if (goal.riskLevel === 'low') {
score += this.ruleEngineConfig.programmatic.scoreWeights.risk.low;
breakdown['risk'] = this.ruleEngineConfig.programmatic.scoreWeights.risk.low;
factors.push('Low risk');
}
else if (goal.riskLevel === 'medium') {
score += this.ruleEngineConfig.programmatic.scoreWeights.risk.medium;
breakdown['risk'] = this.ruleEngineConfig.programmatic.scoreWeights.risk.medium;
factors.push('Medium risk');
}
else {
score += this.ruleEngineConfig.programmatic.scoreWeights.risk.high;
breakdown['risk'] = this.ruleEngineConfig.programmatic.scoreWeights.risk.high;
factors.push('High risk penalty');
}
// Factor 3: Dependencies
if (!goal.dependencies || goal.dependencies.length === 0) {
score += this.ruleEngineConfig.programmatic.scoreWeights.noDependencies;
breakdown['dependencies'] = this.ruleEngineConfig.programmatic.scoreWeights.noDependencies;
factors.push('No dependencies');
}
// Factor 4: Estimated effort
if (goal.estimatedEffort && goal.estimatedEffort <= this.ruleEngineConfig.programmatic.quickWinHours) {
score += this.ruleEngineConfig.programmatic.scoreWeights.quickWin;
breakdown['effort'] = this.ruleEngineConfig.programmatic.scoreWeights.quickWin;
factors.push(`Quick win (≤${this.ruleEngineConfig.programmatic.quickWinHours} hours)`);
}
// Factor 5: Previous success rate
const previousDecisions = this.state.decisions.filter(d => d.outcome === 'success');
const successRate = previousDecisions.length / Math.max(this.state.decisions.length, 1);
if (successRate > this.ruleEngineConfig.programmatic.successRateThreshold) {
score += this.ruleEngineConfig.programmatic.scoreWeights.successBonus;
breakdown['successRate'] = this.ruleEngineConfig.programmatic.scoreWeights.successBonus;
factors.push(`High success rate (${(successRate * 100).toFixed(0)}%)`);
}
return { score, factors, breakdown };
}
/**
* Validate goal for security threats
*
* IMPORTANT: This is a FIRST LINE OF DEFENSE, not a replacement for LLM judgment.
* False positives are acceptable - this flags potential issues for LLM review.
*
* Think of this like a spam filter: it catches obvious bad content but
* the LLM still needs to make final judgment calls.
*
* @since v2.0.0 - Refactored to return warnings instead of throwing errors
* @since v2.0.0 - Made public for use in AgentManager.executeAgent()
*/
validateGoalSecurity(goal) {
const warnings = [];
const flagged = [];
// CATEGORY 1: Code injection patterns (high confidence)
const codeInjectionPatterns = {
'system() call': /system\s*\(/i,
'exec() call': /exec\s*\(/i,
'eval() call': /eval\s*\(/i,
'require() call': /require\s*\(/i,
'dynamic import': /import\s*\(/i,
'template literal': /\$\{.*\}/,
'backticks': /`.*`/,
'process access': /process\.\w+/i,
'child_process': /child_process/i
};
for (const [name, pattern] of Object.entries(codeInjectionPatterns)) {
if (pattern.test(goal)) {
warnings.push(`Possible code injection: ${name}`);
flagged.push(name);
}
}
// CATEGORY 2: Suspicious keywords (lower confidence - advisory only)
const suspiciousKeywords = {
'credentials': /password|credential|secret|token|api[_-]?key/i,
'malicious intent': /hack|exploit|breach|attack/i,
'destructive action': /delete\s+all|destroy|wipe|erase\s+everything/i,
'theft keywords': /steal|theft|rob/i
};
for (const [name, pattern] of Object.entries(suspiciousKeywords)) {
if (pattern.test(goal)) {
warnings.push(`Suspicious keyword: ${name}`);
flagged.push(name);
}
}
// Return warnings but don't block - let LLM decide
return {
safe: warnings.length === 0,
warnings: warnings.length > 0 ? warnings : undefined,
flagged: flagged.length > 0 ? flagged : undefined
};
}
/**
* Commit persisted state version after successful save.
*
* This Symbol-keyed method provides runtime privacy - it can only be called
* by code that has access to the COMMIT_PERSISTED_VERSION symbol (i.e., AgentManager).
* External code cannot call this method without the Symbol.
*
* @param version - The new version number to set
* @see Issue #24 - Optimistic locking implementation
* @see Issue #123 - Option C pattern: version increments only on successful save
*/
[COMMIT_PERSISTED_VERSION](version) {
this.state.stateVersion = version;
}
/**
* Get agent state
*/
getState() {
return { ...this.state };
}
/**
* Update agent context
*/
updateContext(key, value) {
const sanitizedKey = sanitizeInput(key, 50);
// Validate context size
const contextStr = JSON.stringify({ ...this.state.context, [sanitizedKey]: value });
if (contextStr.length > AGENT_LIMITS.MAX_CONTEXT_LENGTH) {
throw ErrorHandler.createError(`Context size exceeds maximum of ${AGENT_LIMITS.MAX_CONTEXT_LENGTH} characters`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.CONTEXT_TOO_LARGE);
}
this.state.context[sanitizedKey] = value;
// Note: stateVersion is incremented on successful save, not here (Issue #123 fix)
this.isDirtyState = true;
this.markDirty();
}
/**
* Complete a goal
*/
completeGoal(goalId, outcome = 'success') {
const goal = this.state.goals.find(g => g.id === goalId);
if (!goal) {
throw ErrorHandler.createError(`Goal ${goalId} not found`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.GOAL_NOT_FOUND);
}
goal.status = outcome === 'success' ? 'completed' : 'failed';
goal.completedAt = new Date();
goal.updatedAt = new Date();
// Update actual effort if it was being tracked
if (goal.estimatedEffort && goal.createdAt) {
const hoursElapsed = (goal.completedAt.getTime() - goal.createdAt.getTime()) / (1000 * 60 * 60);
goal.actualEffort = Math.round(hoursElapsed * 10) / 10;
}
// Update decision outcomes
const decisions = this.state.decisions.filter(d => d.goalId === goalId);
decisions.forEach(d => {
if (!d.outcome) {
d.outcome = outcome;
}
});
// Note: stateVersion is incremented on successful save, not here (Issue #123 fix)
this.isDirtyState = true;
this.markDirty();
logger.info(`Goal ${goalId} completed with outcome: ${outcome}`);
}
/**
* Detect dependency cycles in goal dependencies
* MEDIUM PRIORITY IMPROVEMENT: Prevents circular dependencies between goals
*/
detectDependencyCycle(newGoalId, dependencies) {
// Build dependency graph including the new goal
const visited = new Set();
const recursionStack = new Set();
const path = [];
// Helper function to perform DFS
const hasCycleDFS = (goalId) => {
visited.add(goalId);
recursionStack.add(goalId);
path.push(goalId);
// Get dependencies for current goal
let deps = [];
if (goalId === newGoalId) {
// For the new goal being added, use provided dependencies
deps = dependencies;
}
else {
// For existing goals, get from state
const goal = this.state.goals.find(g => g.id === goalId);
deps = goal?.dependencies || [];
}
// Check each dependency
for (const depId of deps) {
if (!visited.has(depId)) {
if (hasCycleDFS(depId)) {
return true;
}
}
else if (recursionStack.has(depId)) {
// Found a cycle - add the repeated node to show the cycle
path.push(depId);
return true;
}
}
recursionStack.delete(goalId);
path.pop();
return false;
};
// Check if adding this goal would create a cycle
const hasCycle = hasCycleDFS(newGoalId);
return {
hasCycle,
path: hasCycle ? path : []
};
}
/**
* Get goals by status
*/
getGoalsByStatus(status) {
return this.state.goals.filter(g => g.status === status);
}
/**
* Get goals by quadrant
*/
getGoalsByQuadrant(quadrant) {
return this.state.goals.filter(g => g.eisenhowerQuadrant === quadrant);
}
/**
* Calculate agent performance metrics
* MEDIUM PRIORITY IMPROVEMENT: Enhanced to include decision timing metrics
*/
getPerformanceMetrics() {
const completedGoals = this.state.goals.filter(g => g.status === 'completed');
const failedGoals = this.state.goals.filter(g => g.status === 'failed');
const inProgressGoals = this.state.goals.filter(g => g.status === 'in_progress');
const totalCompleted = completedGoals.length + failedGoals.length;
const successRate = totalCompleted > 0 ? completedGoals.length / totalCompleted : 0;
// Calculate average completion time
let totalTime = 0;
let timeCount = 0;
completedGoals.forEach(goal => {
if (goal.completedAt && goal.createdAt) {
totalTime += goal.completedAt.getTime() - goal.createdAt.getTime();
timeCount++;
}
});
const averageCompletionTime = timeCount > 0 ? totalTime / timeCount / (1000 * 60 * 60) : 0; // in hours
// Calculate decision accuracy
const decisionsWithOutcome = this.state.decisions.filter(d => d.outcome);
const successfulDecisions = decisionsWithOutcome.filter(d => d.outcome === 'success');
const decisionAccuracy = decisionsWithOutcome.length > 0
? successfulDecisions.length / decisionsWithOutcome.length
: 0;
// Calculate average decision timing metrics
const decisionsWithMetrics = this.state.decisions.filter(d => d.performanceMetrics);
let avgDecisionTime = 0;
let avgFrameworkTime = 0;
let avgRiskTime = 0;
if (decisionsWithMetrics.length > 0) {
const totalDecisionTime = decisionsWithMetrics.reduce((sum, d) => sum + (d.performanceMetrics?.decisionTimeMs || 0), 0);
const totalFrameworkTime = decisionsWithMetrics.reduce((sum, d) => sum + (d.performanceMetrics?.frameworkTimeMs || 0), 0);
const totalRiskTime = decisionsWithMetrics.reduce((sum, d) => sum + (d.performanceMetrics?.riskAssessmentTimeMs || 0), 0);
avgDecisionTime = totalDecisionTime / decisionsWithMetrics.length;
avgFrameworkTime = totalFrameworkTime / decisionsWithMetrics.length;
avgRiskTime = totalRiskTime / decisionsWithMetrics.length;
}
return {
successRate,
averageCompletionTime,
goalsCompleted: completedGoals.length,
goalsInProgress: inProgressGoals.length,
decisionAccuracy,
averageDecisionTimeMs: decisionsWithMetrics.length > 0 ? avgDecisionTime : undefined,
averageFrameworkTimeMs: decisionsWithMetrics.length > 0 ? avgFrameworkTime : undefined,
averageRiskAssessmentTimeMs: decisionsWithMetrics.length > 0 ? avgRiskTime : undefined
};
}
/**
* Validate the agent
*/
validate() {
const result = super.validate();
const errors = result.errors || [];
const warnings = result.warnings || [];
const suggestions = result.suggestions || [];
// Validate decision framework
if (this.extensions?.decisionFramework && !DECISION_FRAMEWORKS.includes(this.extensions.decisionFramework)) {
errors.push({
field: 'extensions.decisionFramework',
message: `Invalid decision framework. Must be one of: ${DECISION_FRAMEWORKS.join(', ')}`
});
}
// Validate risk tolerance
if (this.extensions?.riskTolerance && !RISK_TOLERANCE_LEVELS.includes(this.extensions.riskTolerance)) {
errors.push({
field: 'extensions.riskTolerance',
message: `Invalid risk tolerance. Must be one of: ${RISK_TOLERANCE_LEVELS.join(', ')}`
});
}
// Validate state size
const stateSize = JSON.stringify(this.state).length;
if (stateSize > AGENT_LIMITS.MAX_STATE_SIZE) {
errors.push({
field: 'state',
message: `State size (${stateSize} bytes) exceeds maximum of ${AGENT_LIMITS.MAX_STATE_SIZE} bytes`
});
}
// Check for orphaned dependencies
const allGoalIds = new Set(this.state.goals.map(g => g.id));
this.state.goals.forEach(goal => {
if (goal.dependencies) {
goal.dependencies.forEach(depId => {
if (!allGoalIds.has(depId)) {
warnings.push({
field: `goal.${goal.id}.dependencies`,
message: `Dependency ${depId} not found`,
severity: 'medium'
});
}
});
}
});
// Suggestions
// Issue #749: Check both v1 runtime goals and v2 metadata goal template
const hasV2Goal = 'goal' in this.metadata && !!this.metadata.goal;
if (this.state.goals.length === 0 && !hasV2Goal) {
suggestions.push('Add some goals to make the agent functional');
}
// Issue #749: Don't suggest deprecated v1 fields on v2 agents
if (!hasV2Goal && (!this.extensions?.specializations || this.extensions.specializations.length === 0)) {
suggestions.push('Consider adding specializations to improve agent focus');
}
const metrics = this.getPerformanceMetrics();
if (metrics.successRate < 0.5 && metrics.goalsCompleted > 5) {
suggestions.push('Low success rate detected. Consider reviewing goal difficulty or decision framework');
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined,
suggestions: suggestions.length > 0 ? suggestions : undefined
};
}
/**
* Serialize to JSON format for internal use and testing
*/
serializeToJSON() {
const data = {
...JSON.parse(super.serializeToJSON()),
state: this.state
};
return JSON.stringify(data, null, 2);
}
/**
* Get content for serialization
*/
getContent() {
let content = `# ${this.metadata.name}\n\n`;
content += `${this.metadata.description}\n\n`;
if (this.state.goals.length > 0) {
content += `## Current Goals\n\n`;
this.state.goals.forEach(goal => {
content += `### ${goal.description}\n`;
content += `- **Priority**: ${goal.priority}\n`;
content += `- **Status**: ${goal.status}\n`;
// Progress tracking could be added in future
// if (goal.progress !== undefined) {
// content += `- **Progress**: ${goal.progress}%\n`;
// }
content += '\n';
});
}
if (this.state.context && Object.keys(this.state.context).length > 0) {
content += `## Context\n\n`;
for (const [key, value] of Object.entries(this.state.context)) {
content += `- **${key}**: ${JSON.stringify(value)}\n`;
}
content += '\n';
}
return content;
}
/**
* Serialize agent to markdown format with YAML frontmatter
* FIX: Changed from JSON to markdown for GitHub portfolio compatibility
*/
serialize() {
// Add agent state to extensions for frontmatter
const originalExtensions = this.extensions;
this.extensions = {
...originalExtensions,
state: this.state
};
// Use base class serialize which now outputs markdown
const result = super.serialize();
// Restore original extensions
this.extensions = originalExtensions;
return result;
}
/**
* Deserialize agent including state
*/
deserialize(data) {
const validationResult = UnicodeValidator.normalize(data);
const parsed = JSON.parse(validationResult.normalizedContent);
// Deserialize base properties
super.deserialize(JSON.stringify({
id: parsed.id,
type: parsed.type,
version: parsed.version,
metadata: parsed.metadata,
references: parsed.references,
extensions: parsed.extensions,
ratings: parsed.ratings
}));
// Deserialize state with validation
if (parsed.state) {
// Validate state size
const stateStr = JSON.stringify(parsed.state);
if (stateStr.length > AGENT_LIMITS.MAX_STATE_SIZE) {
throw ErrorHandler.createError(`State size exceeds maximum of ${AGENT_LIMITS.MAX_STATE_SIZE} bytes`, ErrorCategory.VALIDATION_ERROR, ValidationErrorCodes.STATE_TOO_LARGE);
}
// Restore dates
if (parsed.state.goals) {
parsed.state.goals.forEach((goal) => {
goal.createdAt = new Date(goal.createdAt);
goal.updatedAt = new Date(goal.updatedAt);
if (goal.completedAt) {
goal.completedAt = new Date(goal.completedAt);
}
});
}
if (parsed.state.decisions) {
parsed.state.decisions.forEach((decision) => {
decision.timestamp = new Date(decision.timestamp);
});
}
if (parsed.state.lastActive) {
parsed.state.lastActive = new Date(parsed.state.lastActive);
}
this.state = parsed.state;
// Reconstruct EvictingQueue from deserialized decisions
this._decisionHistory = new EvictingQueue(AGENT_LIMITS.MAX_DECISION_HISTORY);
this._decisionHistory.reset(this.state.decisions || []);
}
this.isDirtyState = false;
}
/**
* Agent activation
*/
async activate() {
await super.activate();
// Update session tracking
this.state.sessionCount++;
this.state.lastActive = new Date();
// Note: stateVersion is incremented on successful save, not here (Issue #123 fix)
this.isDirtyState = true;
// Log activation
logger.info(`Agent ${this.metadata.name} activated`, {
sessionCount: this.state.sessionCount,
activeGoals: this.getGoalsByStatus('in_progress').length
});
}
/**
* Agent deactivation
*/
async deactivate() {
// Save any pending state
if (this.isDirtyState) {
logger.debug(`Agent ${this.metadata.name} has unsaved state changes`);
}
await super.deactivate();
}
/**
* Check if agent needs state persistence
*/
needsStatePersistence() {
return this.isDirtyState;
}
/**
* Mark state as persisted
*/
markStatePersisted() {
this.isDirtyState = false;
}
/**
* Create a goal from a template
* LOW PRIORITY IMPROVEMENT: Goal template system for common patterns
*/
addGoalFromTemplate(templateId, customFields) {
// Apply template to get goal data
const goalData = applyGoalTemplate(templateId, customFields);
// Create goal using the template data
return this.addGoal(goalData);
}
/**
* Get template recommendations based on goal description
*/
getGoalTemplateRecommendations(description) {
return recommendGoalTemplate(description);
}
/**
* Validate a goal against its template
*/
validateGoalTemplate(goalId) {
const goal = this.state.goals.find(g => g.id === goalId);
if (!goal) {
return { valid: false, errors: ['Goal not found'] };
}
// If goal was created from template, validate against it
const templateId = goal.templateId;
return validateGoalAgainstTemplate(goal, templateId);
}
/**
* Update rule engine configuration
*/
updateRuleEngineConfig(config) {
this.ruleEngineConfig = validateRuleEngineConfig({
...this.ruleEngineConfig,
...config
});
// Update extensions
this.extensions = {
...this.extensions,
ruleEngineConfig: this.ruleEngineConfig
};
this.markDirty();
}
/**
* Get current rule engine configuration
*/
getRuleEngineConfig() {
return { ...this.ruleEngineConfig };
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQWdlbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvZWxlbWVudHMvYWdlbnRzL0FnZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7O0dBVUc7QUFFSCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFFaEQsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQ3ZELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDMUMsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBQ2pFLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLCtDQUErQyxDQUFDO0FBQ2pGLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUNwRSxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDL0MsT0FBTyxFQUFFLFlBQVksRUFBRSxhQUFhLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUMxRSxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDN0QsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFhakUsT0FBTyxFQUNMLFlBQVksRUFDWixjQUFjLEVBQ2QsZ0JBQWdCLEVBQ2hCLG1CQUFtQixFQUNuQixxQkFBcUIsRUFDckIsd0JBQXdCLEVBQ3pCLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEIsT0FBTyxFQUVMLHdCQUF3QixFQUN6QixNQUFNLHVCQUF1QixDQUFDO0FBQy9CLE9BQU8sRUFDTCxpQkFBaUIsRUFDakIsMkJBQTJCLEVBQzNCLHFCQUFxQixFQUNyQiwyQkFBMkIsRUFDNUIsTUFBTSxvQkFBb0IsQ0FBQztBQUU1QixNQUFNLE9BQU8sS0FBTSxTQUFRLFdBQVc7SUFFcEMscUZBQXFGO0lBQzdFLEtBQUssQ0FBYTtJQUNsQixZQUFZLEdBQVksS0FBSyxD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