UNPKG

shipdeck

Version:

Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.

436 lines (383 loc) 13.3 kB
/** * Agent Executor for Shipdeck Ultimate * Handles agent prompt construction, execution, and result processing */ const { AnthropicClient } = require('./client'); const path = require('path'); const fs = require('fs'); // Agent role definitions and system prompts const AGENT_ROLES = { 'backend-architect': { systemPrompt: `You are a master backend architect with deep expertise in designing scalable, secure, and maintainable server-side systems. Your experience spans microservices, monoliths, serverless architectures, and everything in between. CRITICAL RULES YOU MUST FOLLOW: - NEVER use 'any' type in TypeScript - use 'unknown' or specific types - Always provide explicit return types for functions - Always validate request inputs with proper schemas - Implement proper error handling with consistent formats - Use appropriate HTTP status codes - Always handle async errors with try/catch or error middleware - NEVER log sensitive information (passwords, tokens, PII) - Always validate and sanitize user inputs - Follow OWASP security guidelines`, temperature: 0.1, maxTokens: 4000 }, 'frontend-developer': { systemPrompt: `You are an expert frontend developer specializing in modern React applications, TypeScript, and exceptional user experiences. CRITICAL RULES YOU MUST FOLLOW: - NEVER use 'any' type in TypeScript - use proper typing - No async operations in client components - Use 'use client' directive only when needed - Implement error boundaries for all features - Always handle loading and error states - Memoize expensive computations - Lazy load heavy components - Generate tests IMMEDIATELY after writing code`, temperature: 0.2, maxTokens: 4000 }, 'ai-engineer': { systemPrompt: `You are an AI engineer specializing in LLM integration, prompt engineering, and intelligent system design. CRITICAL RULES YOU MUST FOLLOW: - Design prompts for clarity and specificity - Implement proper token management and cost optimization - Handle streaming responses gracefully - Build robust error handling for API failures - Implement context window management - Create fallback strategies for AI failures`, temperature: 0.1, maxTokens: 4000 }, 'test-writer-fixer': { systemPrompt: `You are a comprehensive testing specialist who writes thorough test suites for all code. CRITICAL RULES YOU MUST FOLLOW: - Generate tests IMMEDIATELY after creating endpoints - Include unit tests for business logic - Add integration tests for API endpoints - Test error scenarios and edge cases - Mock external dependencies properly - Achieve minimum 80% coverage - Use proper test structure and naming`, temperature: 0.1, maxTokens: 3000 }, 'devops-automator': { systemPrompt: `You are a DevOps automation specialist focusing on CI/CD, deployment, and infrastructure management. CRITICAL RULES YOU MUST FOLLOW: - Create Dockerized applications with multi-stage builds - Implement health checks and monitoring - Set up proper logging and tracing - Create CI/CD-friendly architectures - Implement feature flags for safe deployments - Design for zero-downtime deployments`, temperature: 0.1, maxTokens: 3500 } }; // Context management for long conversations class ConversationContext { constructor(maxContextSize = 150000) { this.messages = []; this.maxContextSize = maxContextSize; this.summaryCache = new Map(); } addMessage(role, content) { this.messages.push({ role, content, timestamp: Date.now() }); this._pruneContext(); } getMessages() { return this.messages.map(({ role, content }) => ({ role, content })); } _pruneContext() { // Estimate total context size const totalSize = this.messages.reduce((sum, msg) => sum + (typeof msg.content === 'string' ? msg.content.length : JSON.stringify(msg.content).length), 0); if (totalSize > this.maxContextSize) { // Keep the system message and recent messages, summarize the middle const systemMessages = this.messages.filter(msg => msg.role === 'system'); const recentMessages = this.messages.slice(-10); const middleMessages = this.messages.slice(systemMessages.length, -10); if (middleMessages.length > 0) { const summary = this._createSummary(middleMessages); this.messages = [ ...systemMessages, { role: 'assistant', content: `[Previous conversation summary: ${summary}]`, timestamp: Date.now() }, ...recentMessages ]; } } } _createSummary(messages) { // Simple summarization - in production you might want to use AI for this const keyPoints = []; for (const msg of messages) { if (msg.content.length > 100) { keyPoints.push(msg.content.substring(0, 100) + '...'); } } return keyPoints.join('; '); } clear() { this.messages = []; this.summaryCache.clear(); } } class AgentExecutor { constructor(options = {}) { this.client = new AnthropicClient(options.anthropic); this.contexts = new Map(); // Agent ID -> ConversationContext this.config = { defaultAgent: 'backend-architect', contextTimeout: 30 * 60 * 1000, // 30 minutes autoGenerateTests: true, ...options.config }; // Set up context cleanup this._setupContextCleanup(); } /** * Setup periodic context cleanup */ _setupContextCleanup() { setInterval(() => { const now = Date.now(); for (const [agentId, context] of this.contexts) { const lastMessageTime = context.messages[context.messages.length - 1]?.timestamp || 0; if (now - lastMessageTime > this.config.contextTimeout) { this.contexts.delete(agentId); } } }, 5 * 60 * 1000); // Check every 5 minutes } /** * Get or create conversation context for an agent */ _getContext(agentId) { if (!this.contexts.has(agentId)) { this.contexts.set(agentId, new ConversationContext()); } return this.contexts.get(agentId); } /** * Execute an agent with a specific prompt */ async executeAgent(agentType, prompt, options = {}) { if (!AGENT_ROLES[agentType]) { throw new Error(`Unknown agent type: ${agentType}. Available agents: ${Object.keys(AGENT_ROLES).join(', ')}`); } const agentConfig = AGENT_ROLES[agentType]; const agentId = options.sessionId || `${agentType}-${Date.now()}`; const context = this._getContext(agentId); // Add system prompt if this is a new conversation if (context.messages.length === 0) { context.addMessage('system', agentConfig.systemPrompt); } // Add user prompt context.addMessage('user', prompt); // Prepare request options const requestOptions = { model: options.model || this.client.model, maxTokens: options.maxTokens || agentConfig.maxTokens, temperature: options.temperature ?? agentConfig.temperature, stream: options.stream || false, ...options.anthropicOptions }; try { const response = await this.client.createMessage( context.getMessages(), requestOptions ); // Add assistant response to context const assistantContent = response.content[0]?.text || ''; context.addMessage('assistant', assistantContent); // Auto-generate tests if enabled and this was a code generation if (this.config.autoGenerateTests && this._isCodeGeneration(assistantContent) && agentType !== 'test-writer-fixer') { try { const testResponse = await this.executeAgent('test-writer-fixer', `Generate comprehensive tests for the following code:\n\n${assistantContent}`, { sessionId: `${agentId}-tests`, stream: false } ); response._tests = testResponse.content[0]?.text; } catch (testError) { console.warn('Failed to auto-generate tests:', testError.message); } } return { content: response.content, usage: response.usage, metadata: { ...response._metadata, agentType, sessionId: agentId, hasTests: !!response._tests }, tests: response._tests }; } catch (error) { throw new Error(`Agent execution failed for ${agentType}: ${error.message}`); } } /** * Execute an agent with streaming response */ async executeAgentStream(agentType, prompt, options = {}) { const streamOptions = { ...options, stream: true }; const agentConfig = AGENT_ROLES[agentType]; const agentId = options.sessionId || `${agentType}-${Date.now()}`; const context = this._getContext(agentId); // Add system prompt if this is a new conversation if (context.messages.length === 0) { context.addMessage('system', agentConfig.systemPrompt); } // Add user prompt context.addMessage('user', prompt); const stream = await this.client.createStreamingMessage( context.getMessages(), { ...streamOptions, ...agentConfig } ); // Wrap stream to add agent metadata and context management return this._wrapAgentStream(stream, agentType, agentId, context); } /** * Wrap stream with agent-specific functionality */ _wrapAgentStream(stream, agentType, agentId, context) { let fullContent = ''; return { async *[Symbol.asyncIterator]() { try { for await (const chunk of stream) { if (chunk.type === 'content') { fullContent = chunk.fullContent; yield { ...chunk, agent: { type: agentType, sessionId: agentId } }; } else if (chunk.type === 'usage') { yield { ...chunk, agent: { type: agentType, sessionId: agentId } }; } } // Add response to context if (fullContent) { context.addMessage('assistant', fullContent); } } catch (error) { throw new Error(`Agent stream failed for ${agentType}: ${error.message}`); } }, async getAllContent() { for await (const chunk of this) { // Stream will automatically populate fullContent } return fullContent; } }; } /** * Execute multiple agents in parallel */ async executeAgentsParallel(agentTasks) { const promises = agentTasks.map(async (task) => { try { const result = await this.executeAgent(task.agent, task.prompt, task.options); return { success: true, agent: task.agent, result }; } catch (error) { return { success: false, agent: task.agent, error: error.message }; } }); const results = await Promise.allSettled(promises); return results.map(result => result.value); } /** * Check if content appears to be code generation */ _isCodeGeneration(content) { const codeIndicators = [ /```[\w]*\n/g, // Code blocks /function\s+\w+/g, // Function declarations /class\s+\w+/g, // Class declarations /interface\s+\w+/g, // Interface declarations /import\s+.*from/g, // Imports /export\s+(default\s+)?/g, // Exports ]; return codeIndicators.some(regex => regex.test(content)); } /** * Get agent conversation history */ getAgentHistory(agentId) { const context = this.contexts.get(agentId); return context ? context.getMessages() : []; } /** * Clear agent conversation history */ clearAgentHistory(agentId) { if (this.contexts.has(agentId)) { this.contexts.get(agentId).clear(); } } /** * Get all active agent sessions */ getActiveSessions() { return Array.from(this.contexts.keys()).map(agentId => ({ agentId, messageCount: this.contexts.get(agentId).messages.length, lastActivity: Math.max(...this.contexts.get(agentId).messages.map(m => m.timestamp)) })); } /** * Get available agents */ getAvailableAgents() { return Object.keys(AGENT_ROLES).map(agentType => ({ type: agentType, description: AGENT_ROLES[agentType].systemPrompt.split('\n')[0], config: { temperature: AGENT_ROLES[agentType].temperature, maxTokens: AGENT_ROLES[agentType].maxTokens } })); } /** * Estimate cost for agent execution */ estimateAgentCost(agentType, prompt, options = {}) { if (!AGENT_ROLES[agentType]) { throw new Error(`Unknown agent type: ${agentType}`); } const agentConfig = AGENT_ROLES[agentType]; const systemPrompt = agentConfig.systemPrompt; const messages = [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ]; return this.client.estimateMessageCost(messages, { ...options, maxTokens: options.maxTokens || agentConfig.maxTokens }); } /** * Get client usage statistics */ getUsageStats() { return this.client.getUsage(); } /** * Reset usage statistics */ resetUsageStats() { this.client.resetUsage(); } } module.exports = { AgentExecutor, AGENT_ROLES, ConversationContext };