UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

360 lines (359 loc) • 16.2 kB
import { sseNotifier } from '../../services/sse-notifier/index.js'; import { jobManager } from '../../services/job-manager/index.js'; import { getTaskOperations } from '../vibe-task-manager/core/operations/task-operations.js'; import { registerTool } from '../../services/routing/toolRegistry.js'; import { dependencyContainer } from '../../services/dependency-container.js'; import { z } from 'zod'; class AgentResponseProcessor { static instance; static isInitializing = false; responseHistory = new Map(); agentRegistryCache = null; agentTaskQueueCache = null; static getInstance() { if (AgentResponseProcessor.isInitializing) { console.warn('Circular initialization detected in AgentResponseProcessor, using safe fallback'); return AgentResponseProcessor.createSafeFallback(); } if (!AgentResponseProcessor.instance) { AgentResponseProcessor.isInitializing = true; try { AgentResponseProcessor.instance = new AgentResponseProcessor(); } finally { AgentResponseProcessor.isInitializing = false; } } return AgentResponseProcessor.instance; } static createSafeFallback() { const fallback = Object.create(AgentResponseProcessor.prototype); fallback.responseHistory = new Map(); fallback.processResponse = async () => { console.warn('AgentResponseProcessor fallback: processResponse called during initialization'); }; fallback.getResponse = async () => { console.warn('AgentResponseProcessor fallback: getResponse called during initialization'); return undefined; }; fallback.getAllResponses = async () => { console.warn('AgentResponseProcessor fallback: getAllResponses called during initialization'); return []; }; return fallback; } async getAgentRegistry() { if (!this.agentRegistryCache) { this.agentRegistryCache = await dependencyContainer.getAgentRegistry(); } return this.agentRegistryCache; } async getAgentTaskQueue() { if (!this.agentTaskQueueCache) { this.agentTaskQueueCache = await dependencyContainer.getAgentTaskQueue(); } return this.agentTaskQueueCache; } async processResponse(response) { try { await this.validateResponse(response); response.receivedAt = Date.now(); this.responseHistory.set(response.taskId, response); await this.updateTaskStatus(response); await this.updateJobStatus(response); await this.updateAgentStatus(response); await this.broadcastTaskCompletion(response); console.log(`Task ${response.taskId} completed by agent ${response.agentId} with status: ${response.status}`); } catch (error) { console.error('Failed to process agent response:', error); throw error; } } async validateResponse(response) { const agentRegistry = await this.getAgentRegistry(); const agent = agentRegistry ? await agentRegistry.getAgent(response.agentId) : null; if (!agent) { throw new Error(`Agent ${response.agentId} not found`); } const taskQueue = await this.getAgentTaskQueue(); const task = taskQueue ? await taskQueue.getTask(response.taskId) : null; if (!task) { throw new Error(`Task ${response.taskId} not found`); } if (task.agentId !== response.agentId) { throw new Error(`Task ${response.taskId} is not assigned to agent ${response.agentId}`); } if (!['DONE', 'ERROR', 'PARTIAL'].includes(response.status)) { throw new Error(`Invalid response status: ${response.status}`); } if (!response.response || response.response.trim() === '') { throw new Error('Response content is required'); } } async updateTaskStatus(response) { try { const taskOps = getTaskOperations(); let taskStatus; switch (response.status) { case 'DONE': taskStatus = 'completed'; break; case 'ERROR': taskStatus = 'failed'; break; case 'PARTIAL': taskStatus = 'in_progress'; break; default: taskStatus = 'failed'; } await taskOps.updateTaskStatus(response.taskId, taskStatus, response.agentId); if (response.completionDetails) { await taskOps.updateTaskMetadata(response.taskId, { agentResponse: response.response, completionDetails: response.completionDetails, completedBy: response.agentId, completedAt: response.receivedAt, executionTime: response.completionDetails.executionTime, filesModified: response.completionDetails.filesModified, testsPass: response.completionDetails.testsPass, buildSuccessful: response.completionDetails.buildSuccessful }, response.agentId); } } catch (error) { console.error('Failed to update task status:', error); } } async updateJobStatus(response) { try { const result = { isError: response.status !== 'DONE', content: [{ type: 'text', text: JSON.stringify({ success: response.status === 'DONE', result: response.response, details: response.completionDetails, completedBy: response.agentId, completedAt: response.receivedAt }) }] }; jobManager.setJobResult(response.taskId, result); } catch (error) { console.error('Failed to update job status:', error); } } async updateAgentStatus(response) { try { const agentRegistry = await this.getAgentRegistry(); const taskQueue = await this.getAgentTaskQueue(); if (taskQueue) { await taskQueue.removeTask(response.taskId); } const agent = agentRegistry ? await agentRegistry.getAgent(response.agentId) : null; if (agent && agentRegistry && taskQueue) { agent.lastSeen = Date.now(); const queueLength = await taskQueue.getQueueLength(response.agentId); agent.currentTasks = Array.from({ length: queueLength }, (_, i) => `pending-${i + 1}`); const maxTasks = agent.maxConcurrentTasks || 1; if (queueLength < maxTasks && agent.status === 'busy') { await agentRegistry.updateAgentStatus(response.agentId, 'online'); } } } catch (error) { console.error('Failed to update agent status:', error); } } async broadcastTaskCompletion(response) { try { await sseNotifier.broadcastEvent('taskCompleted', { agentId: response.agentId, taskId: response.taskId, status: response.status, completedAt: response.receivedAt, success: response.status === 'DONE', executionTime: response.completionDetails?.executionTime, filesModified: response.completionDetails?.filesModified?.length || 0 }); const agentRegistry = await this.getAgentRegistry(); const agent = agentRegistry ? await agentRegistry.getAgent(response.agentId) : null; if (agent?.transportType === 'sse' && agent.sessionId) { await sseNotifier.sendEvent(agent.sessionId, 'responseReceived', { taskId: response.taskId, acknowledged: true, nextAction: 'ready_for_new_task', timestamp: response.receivedAt }); } } catch (error) { console.error('Failed to broadcast task completion:', error); } } async getResponse(taskId) { return this.responseHistory.get(taskId); } async getAgentResponses(agentId) { return Array.from(this.responseHistory.values()) .filter(response => response.agentId === agentId); } async getResponseStats() { const responses = Array.from(this.responseHistory.values()); const total = responses.length; const successful = responses.filter(r => r.status === 'DONE').length; const failed = responses.filter(r => r.status === 'ERROR').length; const partial = responses.filter(r => r.status === 'PARTIAL').length; const executionTimes = responses .map(r => r.completionDetails?.executionTime) .filter(time => typeof time === 'number'); const averageExecutionTime = executionTimes.length > 0 ? executionTimes.reduce((sum, time) => sum + time, 0) / executionTimes.length : 0; return { total, successful, failed, partial, averageExecutionTime }; } } export const submitTaskResponseTool = { name: 'submit-task-response', description: 'Submit task completion response from agent', inputSchema: { type: 'object', properties: { agentId: { type: 'string', description: 'Agent identifier' }, taskId: { type: 'string', description: 'Task identifier' }, status: { type: 'string', enum: ['DONE', 'ERROR', 'PARTIAL'], description: 'Task completion status' }, response: { type: 'string', description: 'Sentinel protocol response content' }, completionDetails: { type: 'object', properties: { filesModified: { type: 'array', items: { type: 'string' }, description: 'List of files modified during task execution' }, testsPass: { type: 'boolean', description: 'Whether all tests passed' }, buildSuccessful: { type: 'boolean', description: 'Whether build was successful' }, executionTime: { type: 'number', description: 'Task execution time in milliseconds' }, errorDetails: { type: 'string', description: 'Error details if status is ERROR' }, partialProgress: { type: 'number', minimum: 0, maximum: 100, description: 'Completion percentage if status is PARTIAL' } } } }, required: ['agentId', 'taskId', 'status', 'response'] } }; export async function handleSubmitTaskResponse(args) { try { const response = { agentId: args.agentId, taskId: args.taskId, status: args.status, response: args.response, completionDetails: args.completionDetails }; const processor = AgentResponseProcessor.getInstance(); await processor.processResponse(response); const statusEmoji = response.status === 'DONE' ? 'āœ…' : response.status === 'ERROR' ? 'āŒ' : 'ā³'; const completionInfo = response.completionDetails ? [ response.completionDetails.filesModified ? `Files Modified: ${response.completionDetails.filesModified.length}` : '', response.completionDetails.testsPass !== undefined ? `Tests: ${response.completionDetails.testsPass ? 'PASS' : 'FAIL'}` : '', response.completionDetails.buildSuccessful !== undefined ? `Build: ${response.completionDetails.buildSuccessful ? 'SUCCESS' : 'FAIL'}` : '', response.completionDetails.executionTime ? `Execution Time: ${response.completionDetails.executionTime}ms` : '', response.completionDetails.partialProgress ? `Progress: ${response.completionDetails.partialProgress}%` : '' ].filter(info => info !== '').join('\n') : ''; return { content: [{ type: 'text', text: `${statusEmoji} Task Response Submitted Successfully\n\n` + `Agent: ${response.agentId}\n` + `Task: ${response.taskId}\n` + `Status: ${response.status}\n` + `Submitted: ${new Date().toISOString()}\n` + `${completionInfo ? `\nšŸ“Š Completion Details:\n${completionInfo}\n` : ''}` + `\nšŸ”§ Next Steps:\n` + `- Task has been marked as ${response.status.toLowerCase()}\n` + `- Job status updated for client polling\n` + `- Continue polling for new task assignments\n` + `${response.status === 'PARTIAL' ? '- Submit additional responses as work progresses' : ''}` }] }; } catch (error) { console.error('Submit task response failed:', error); return { content: [{ type: 'text', text: `āŒ Task Response Submission Failed\n\nError: ${error instanceof Error ? error.message : 'Unknown error'}\n\n` + `Please verify:\n` + `- Agent is registered and active\n` + `- Task ID is valid and assigned to this agent\n` + `- Response format follows Sentinel Protocol\n` + `- All required fields are provided` }], isError: true }; } } export { AgentResponseProcessor }; const submitTaskResponseInputSchemaShape = { agentId: z.string().min(1, { message: "Agent ID is required" }).describe("Agent identifier"), taskId: z.string().min(1, { message: "Task ID is required" }).describe("Task identifier"), status: z.enum(['DONE', 'ERROR', 'PARTIAL']).describe("Task completion status"), response: z.string().min(1, { message: "Response content is required" }).describe("Sentinel protocol response content"), completionDetails: z.object({ filesModified: z.array(z.string()).optional().describe("List of files modified during task execution"), testsPass: z.boolean().optional().describe("Whether all tests passed"), buildSuccessful: z.boolean().optional().describe("Whether build was successful"), executionTime: z.number().optional().describe("Task execution time in milliseconds"), errorDetails: z.string().optional().describe("Error details if status is ERROR"), partialProgress: z.number().min(0).max(100).optional().describe("Completion percentage if status is PARTIAL") }).optional().describe("Additional completion details") }; const submitTaskResponseToolDefinition = { name: "submit-task-response", description: "Submit task completion response from agent. Supports both stdio and SSE transports for universal agent communication.", inputSchema: submitTaskResponseInputSchemaShape, executor: handleSubmitTaskResponse }; registerTool(submitTaskResponseToolDefinition);