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
JavaScript
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);