UNPKG

@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.

540 lines 20.3 kB
/** * AgentManager - Refactored to extend BaseElementManager * Manages agent CRUD operations, metadata sanitization, and state persistence. */ import { FileOperationsService } from '../../services/FileOperationsService.js'; import { Agent } from './Agent.js'; import { AgentMetadata, AgentState, ExecuteAgentResult, AgentMetadataV2, AutonomyDirective } from './types.js'; import { type GatheredData } from './gatheredData.js'; import { BaseElementManager } from '../base/BaseElementManager.js'; import { FileLockManager } from '../../security/fileLockManager.js'; import { PortfolioManager } from '../../portfolio/PortfolioManager.js'; import { ValidationRegistry } from '../../services/validation/ValidationRegistry.js'; import { SerializationService } from '../../services/SerializationService.js'; import { MetadataService } from '../../services/MetadataService.js'; import { FileWatchService } from '../../services/FileWatchService.js'; interface ElementCreationResult { success: boolean; message: string; element?: Agent; } type AgentCreateMetadata = (Partial<AgentMetadata> & Partial<AgentMetadataV2>) & { content?: string; }; export declare class AgentManager extends BaseElementManager<Agent> { private readonly stateDir; private readonly stateCache; private triggerValidationService; private validationService; private serializationService; private metadataService; private activeAgentNames; private static elementManagerResolver?; private static dangerZoneEnforcerResolver?; private static verificationStoreResolver?; constructor(portfolioManager: PortfolioManager, fileLockManager: FileLockManager, baseDir: string, fileOperationsService: FileOperationsService, validationRegistry: ValidationRegistry, serializationService: SerializationService, metadataService: MetadataService, fileWatchService?: FileWatchService, memoryBudget?: import('../../cache/CacheMemoryBudget.js').CacheMemoryBudget, backupService?: import('../../services/BackupService.js').BackupService); protected getElementLabel(): string; /** * Configure the element manager resolver for element-agnostic activation * This is called by the DI container during initialization * Follows the same pattern as Memory.configureMemoryManagerResolver * * @param resolver Function that takes a manager name and returns the manager instance */ static setElementManagerResolver(resolver: (managerName: string) => any): void; /** * Issue #402: Set DangerZoneEnforcer resolver for DI injection. * Called by the DI container during initialization. */ static setDangerZoneEnforcerResolver(resolver: () => import('./types.js').DangerZoneBlocker): void; /** * Issue #142: Set VerificationStore resolver for DI injection. * Called by the DI container during initialization. */ static setVerificationStoreResolver(resolver: () => { set: (id: string, challenge: { code: string; expiresAt: number; reason: string; }) => void; }): void; /** * Get the element manager resolver * @private */ private static getElementManagerResolver; /** * Reset static resolvers (for test cleanup) * Call this in afterEach hooks to prevent test isolation issues */ static resetResolvers(): void; /** * Prepare directory structure for agents and state files. */ initialize(): Promise<void>; /** * Create a new agent on disk. */ create(name: string, description: string, content: string, metadata?: AgentCreateMetadata): Promise<ElementCreationResult>; /** * Read an agent by name (without extension). * * @param name - Agent name (without extension) * @returns Agent instance or null if not found */ read(name: string): Promise<Agent | null>; /** * Fallback for read() when direct file lookup fails. * Searches loaded agents by metadata name using case-insensitive and slug matching. * Logs a warning when a match is found (indicates filename/name mismatch needing cleanup). * * @example * // File on disk: "legacy-poster-agent.md" * // Metadata name: "legacy-poster" * // Direct lookup for "legacy-poster.md" fails (ENOENT) * // Flexible fallback matches via metadata name: * const agent = await read("legacy-poster"); // resolves via fallback */ private readFlexibly; /** * Update metadata/content for an existing agent. */ update(name: string, updates: Partial<AgentMetadata>, content?: string): Promise<boolean>; /** * Validate a provided agent name. */ validateName(name: string): { valid: boolean; error?: string; }; /** * Import an agent from serialized content. */ importElement(data: string, format?: 'json' | 'yaml' | 'markdown'): Promise<Agent>; /** * Export an agent to JSON or markdown (default). */ exportElement(agent: Agent, format?: 'json' | 'yaml' | 'markdown'): Promise<string>; /** * Load an agent file, enforcing size and format checks. */ load(filePath: string): Promise<Agent>; /** * Override BaseElementManager.save to persist state when required. */ save(agent: Agent, filePath: string): Promise<void>; /** * Persist agent state to disk (public API for external callers). * * This method is the proper way for external code (strategies, handlers) to * trigger state persistence. It implements the Option C pattern from Issue #123: * stateVersion is only incremented on successful save. * * @param name - Agent name * @returns Promise<boolean> - True if state was persisted, false if not needed * @throws Error if agent not found or save fails */ persistState(name: string): Promise<boolean>; /** * Override delete to remove associated state file. * * FIX: Uses normalizeFilename() to ensure state file deletion matches * the normalized filename used for state file creation/loading. */ delete(filePath: string): Promise<void>; exists(filePath: string): Promise<boolean>; validatePath(targetPath: string): boolean; getFileExtension(): string; /** * Override list to apply active status based on activeAgentNames set */ list(): Promise<Agent[]>; /** * Activate an agent by name or identifier * * Issue #24 (LOW PRIORITY): Performance optimization using findByName() * Issue #24 (LOW PRIORITY): Consistent error messages using ElementMessages * Issue #24 (LOW PRIORITY): Cleanup trigger for memory leak prevention */ activateAgent(identifier: string): Promise<{ success: boolean; message: string; agent?: Agent; }>; /** * Deactivate an agent by name or identifier * * Issue #24 (LOW PRIORITY): Performance optimization using findByName() * Issue #24 (LOW PRIORITY): Consistent error messages using ElementMessages */ deactivateAgent(identifier: string): Promise<{ success: boolean; message: string; }>; /** * Get all active agents */ getActiveAgents(): Promise<Agent[]>; /** * Execute an agent with goal parameters * * Returns context for LLM to drive the agentic loop. * This method: * 1. Loads the agent configuration * 2. Validates and renders the goal template with parameters * 3. Activates configured elements (element-agnostic) * 4. Evaluates programmatic constraints and risk * 5. Returns structured context for LLM * * The LLM then drives the agentic loop using this context. * * @since v2.0.0 - Agentic Loop Redesign */ executeAgent(name: string, parameters: Record<string, unknown>, context?: { operationName?: 'execute_agent' | 'continue_execution'; }): Promise<ExecuteAgentResult>; /** * Security validation for template parameters (Issue #103). * Checks for prototype pollution, Unicode injection, and oversized payloads. * Must be called BEFORE template rendering. * @private */ private validateParameterSecurity; /** * Validate parameters against goal parameter schema * @private */ private validateParameters; /** * Build an actionable missing-parameter error for execute/continue calls. * @private */ private formatMissingRequiredParametersError; /** * Render goal template by replacing {parameter} placeholders * @private */ private renderGoalTemplate; /** * Detect unmatched {placeholder} patterns remaining after template rendering. * Returns array of placeholder names found in the rendered string. * Issue #126: Warn when template parameters are missing. * @private */ private detectUnmatchedPlaceholders; /** * Formats a consistent error message for circular activation detection. * Used by both the static pre-flight check and the runtime chain check. */ private static formatCircularActivationError; /** Safety limits for activation graph traversal */ private static readonly MAX_ACTIVATION_DEPTH; private static readonly MAX_NODES_VISITED; /** * Static activation cycle detection — Layer 1 of dual-detection strategy (Issue #374) * * Performs async DFS on the activation graph to detect cycles BEFORE execution begins. * This is a pre-flight check that prevents circular activation chains from being attempted. * * Layer 1 (this method): Static graph analysis — walks the activation graph from the root * agent, loading each agent's `activates.agents` metadata. Catches all cycles reachable * from the root without executing any agents. Bounded by MAX_ACTIVATION_DEPTH and * MAX_NODES_VISITED to prevent resource exhaustion from large acyclic graphs. * * Layer 2 (getElementContent runtime check): Defense-in-depth — guards against edge cases * like dynamically-constructed activation chains or agents modified between the static * check and actual execution. See getElementContent for details. * * @param rootName - The root agent name initiating the activation chain * @param activatedAgents - The immediate agents activated by the root * @returns The cycle path array if a cycle exists, null otherwise * @private */ private detectActivationCycles; /** * Get content from an element (element-agnostic) * @private */ private getElementContent; /** * Check if active set cleanup is needed and perform cleanup if necessary * Issue #24 (LOW PRIORITY): Memory leak prevention * @private */ private checkAndCleanupActiveSet; /** * Clean up stale entries from active agents set * Issue #24 (LOW PRIORITY): Memory leak prevention * @private */ private cleanupStaleActiveAgents; /** * Persist an agent state file (YAML). * * IMPROVEMENT: Optimistic locking with version checking (Issue #24) * Prevents state corruption from concurrent updates by comparing versions * * FIX: Uses normalizeFilename() for consistent state file naming * This ensures state files use kebab-case (e.g., "crudv-agent-delta.state.yaml") * regardless of input name format (e.g., "CRUDV-Agent-Delta") * * FIX (Issue #107 - CRIT-2): Wrap read-compare-write sequence in file lock * to prevent TOCTOU race condition from concurrent agent executions * * FIX (Issue #123): Version increments on successful save, not during operations * * @returns The new state version after successful save * @protected Only accessible by subclasses (e.g., TestableAgentManager for testing) */ protected saveAgentState(name: string, state: AgentState): Promise<number>; /** * Utility: ensure `.md` extension on requested path. */ private stripExtension; private hydrateAgentState; private loadAgentState; private ensureStateDirectory; protected parseMetadata(data: any): Promise<AgentMetadata>; protected createElement(metadata: AgentMetadata, bodyContent: string): Agent; protected serializeElement(agent: Agent): Promise<string>; private buildDefaultInstructions; private buildDefaultBody; /** * Select the text that should satisfy create-time content validation. * Behavioral instructions remain the primary source, while reference content * acts as the fallback for content-only agent creation. */ private getPrimaryValidationText; /** * Issue #727: Validate V2 agent fields at write time. * * Normalizes snake_case → camelCase (same as parseMetadata) and then validates * structural constraints. Unlike parseMetadata (which silently strips for * backward compat), this returns errors so the caller can reject the create. * * Mutates the metadata in place (normalization), returns validation errors. */ private validateV2FieldsForCreate; /** * Normalize goal input to V2 format. * LLMs may pass goal as a simple string or as a V2 config object. * This ensures we always have a proper V2 structure before validation. * * @param goal - Either a string goal or a V2 goal config object * @returns Normalized V2 goal config, or undefined if no goal provided */ private normalizeGoalInput; private parseAgentFile; /** * Get the filename for an agent element. * * Uses inherited normalizeFilename() from BaseElementManager for consistent * filename formatting across all element managers. * * Examples: * "Creative Writer" -> "creative-writer.md" * "CRUDV-Agent-Delta" -> "crudv-agent-delta.md" * "Multi_Goal_Agent" -> "multi-goal-agent.md" * "CamelCaseName" -> "camel-case-name.md" */ private getFilename; private validateElementName; /** * Get current user for attribution * REFACTORED: Delegates to MetadataService for consistent user attribution across all managers */ private getCurrentUserForAttribution; private prepareStateForSerialization; private normalizeLoadedState; /** * Record a step in agent execution * Wraps Agent.recordDecision() for the MCP tool interface * * @since v2.0.0 - Agentic Loop Redesign */ recordAgentStep(params: { agentName: string; stepDescription: string; outcome: "success" | "failure" | "partial"; /** Optional findings or results from this step */ findings?: string; confidence?: number; /** Optional hint about what the LLM plans to do next (for proactive risk evaluation) */ nextActionHint?: string; /** Optional risk score for the current step (0-100) */ riskScore?: number; /** Runtime override for maxAutonomousSteps (Issue #447) */ maxStepsOverride?: number; }): Promise<{ success: boolean; message: string; decision: { id: string; goalId: string; timestamp: string; decision: string; reasoning: string; framework: string; confidence: number; outcome: "success" | "failure" | "partial"; }; state: { goalCount: number; decisionCount: number; lastActive: string; stateVersion: number; }; /** Autonomy directive indicating whether to continue or pause */ autonomy: AutonomyDirective; }>; /** * Complete an agent goal * Wraps Agent.completeGoal() for the MCP tool interface * * @since v2.0.0 - Agentic Loop Redesign */ completeAgentGoal(params: { agentName: string; goalId?: string; outcome: "success" | "failure" | "partial"; summary: string; }): Promise<{ success: boolean; message: string; goal: { id: string; description: string; status: "completed" | "failed"; createdAt: string; completedAt: string; estimatedEffort?: number; actualEffort?: number; }; metrics: { successRate: number; goalsCompleted: number; goalsInProgress: number; decisionAccuracy: number; averageCompletionTime: number; }; state: { goalCount: number; decisionCount: number; lastActive: string; stateVersion: number; }; }>; /** * Get agent state * Wraps Agent.getState() for the MCP tool interface * * @since v2.0.0 - Agentic Loop Redesign */ getAgentState(params: { agentName: string; includeDecisionHistory?: boolean; includeContext?: boolean; }): Promise<{ success: boolean; agentName: string; state: { goals: Array<{ id: string; description: string; priority: string; status: string; importance: number; urgency: number; eisenhowerQuadrant?: string; createdAt: string; updatedAt: string; completedAt?: string; dependencies?: string[]; riskLevel?: string; estimatedEffort?: number; actualEffort?: number; notes?: string; }>; decisions: Array<{ id: string; goalId: string; timestamp: string; decision: string; reasoning: string; framework: string; confidence: number; outcome?: string; }>; context?: Record<string, any>; contextSummary: { keys: string[]; size: number; }; lastActive: string; sessionCount: number; stateVersion: number; }; metrics: { successRate: number; goalsCompleted: number; goalsInProgress: number; decisionAccuracy: number; averageCompletionTime: number; }; }>; /** * Get gathered data for a specific goal execution. * * Aggregates decision history, goal state, and summary statistics * into a structured GatheredData object. This is a read-side view * over existing agent state data. * * Issue #68: GatheredDataEntry for state recording * @since v2.0.0 - Agentic Loop Completion (Epic #380) */ getGatheredData(params: { agentName: string; goalId: string; }): Promise<GatheredData>; /** * Continue agent execution from previous state * Combines executeAgent with state context * * @since v2.0.0 - Agentic Loop Redesign */ continueAgentExecution(params: { agentName: string; parameters?: Record<string, unknown>; previousStepResult?: string; }): Promise<ExecuteAgentResult & { previousState: { goals: Array<{ id: string; description: string; status: string; progress?: number; }>; recentDecisions: Array<{ decision: string; reasoning: string; outcome?: string; timestamp: string; }>; sessionCount: number; lastActive: string; stateVersion: number; }; continuation: { isResuming: boolean; previousStepResult?: string; suggestedNextSteps?: string[]; }; }>; /** * Generate suggested next steps based on current state * @private */ private generateNextSteps; } export {}; //# sourceMappingURL=AgentManager.d.ts.map