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
JavaScript
/**
* 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 };