292 lines
8.6 KiB
JavaScript
292 lines
8.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
class ElicitationBroker {
|
|
constructor(messageQueue, options = {}) {
|
|
this.messageQueue = messageQueue;
|
|
this.basePath = options.basePath || path.join(process.env.HOME, '.bmad');
|
|
this.sessionsPath = path.join(this.basePath, 'queue', 'elicitation');
|
|
}
|
|
|
|
generateSessionId() {
|
|
return `elicit-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
}
|
|
|
|
async createSession(agentName, initialContext) {
|
|
const session = {
|
|
id: this.generateSessionId(),
|
|
agent: agentName,
|
|
created: new Date().toISOString(),
|
|
status: 'active',
|
|
context: {
|
|
...initialContext,
|
|
elicitationHistory: []
|
|
},
|
|
currentPhase: 'initial',
|
|
metadata: {
|
|
startTime: Date.now(),
|
|
interactionCount: 0
|
|
}
|
|
};
|
|
|
|
const sessionPath = path.join(this.sessionsPath, session.id);
|
|
await fs.mkdir(sessionPath, { recursive: true });
|
|
|
|
await this.saveSession(session);
|
|
return session;
|
|
}
|
|
|
|
async saveSession(session) {
|
|
const sessionPath = path.join(this.sessionsPath, session.id);
|
|
const files = {
|
|
'session.json': session,
|
|
'context.json': session.context,
|
|
'history.json': session.context.elicitationHistory || []
|
|
};
|
|
|
|
for (const [filename, data] of Object.entries(files)) {
|
|
await fs.writeFile(
|
|
path.join(sessionPath, filename),
|
|
JSON.stringify(data, null, 2)
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadSession(sessionId) {
|
|
const sessionPath = path.join(this.sessionsPath, sessionId, 'session.json');
|
|
try {
|
|
const content = await fs.readFile(sessionPath, 'utf8');
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
throw new Error(`Session ${sessionId} not found`);
|
|
}
|
|
}
|
|
|
|
async addQuestion(sessionId, question, metadata = {}) {
|
|
const session = await this.loadSession(sessionId);
|
|
|
|
const questionEntry = {
|
|
id: `q${session.context.elicitationHistory.length + 1}`,
|
|
type: 'question',
|
|
text: question,
|
|
timestamp: new Date().toISOString(),
|
|
metadata,
|
|
phase: session.currentPhase
|
|
};
|
|
|
|
session.context.elicitationHistory.push(questionEntry);
|
|
session.metadata.interactionCount++;
|
|
|
|
await this.saveSession(session);
|
|
return questionEntry;
|
|
}
|
|
|
|
async addResponse(sessionId, response, questionId = null) {
|
|
const session = await this.loadSession(sessionId);
|
|
|
|
const responseEntry = {
|
|
id: `r${session.context.elicitationHistory.filter(h => h.type === 'response').length + 1}`,
|
|
type: 'response',
|
|
text: response,
|
|
questionId,
|
|
timestamp: new Date().toISOString(),
|
|
phase: session.currentPhase
|
|
};
|
|
|
|
session.context.elicitationHistory.push(responseEntry);
|
|
session.metadata.lastResponseTime = Date.now();
|
|
|
|
await this.saveSession(session);
|
|
return responseEntry;
|
|
}
|
|
|
|
async updatePhase(sessionId, newPhase) {
|
|
const session = await this.loadSession(sessionId);
|
|
session.currentPhase = newPhase;
|
|
|
|
session.context.elicitationHistory.push({
|
|
type: 'phase_change',
|
|
from: session.currentPhase,
|
|
to: newPhase,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
await this.saveSession(session);
|
|
}
|
|
|
|
async completeSession(sessionId, finalResult = null) {
|
|
const session = await this.loadSession(sessionId);
|
|
session.status = 'completed';
|
|
session.completedAt = new Date().toISOString();
|
|
session.metadata.endTime = Date.now();
|
|
session.metadata.duration = session.metadata.endTime - session.metadata.startTime;
|
|
|
|
if (finalResult) {
|
|
session.result = finalResult;
|
|
}
|
|
|
|
await this.saveSession(session);
|
|
await this.archiveSession(session);
|
|
|
|
return session;
|
|
}
|
|
|
|
async archiveSession(session) {
|
|
const archivePath = path.join(this.basePath, 'archive', 'elicitation', session.id);
|
|
await fs.mkdir(path.join(this.basePath, 'archive', 'elicitation'), { recursive: true });
|
|
|
|
const sourcePath = path.join(this.sessionsPath, session.id);
|
|
await fs.rename(sourcePath, archivePath);
|
|
}
|
|
|
|
async getActiveElicitations() {
|
|
try {
|
|
const sessionDirs = await fs.readdir(this.sessionsPath);
|
|
const sessions = [];
|
|
|
|
for (const dir of sessionDirs) {
|
|
if (dir.startsWith('elicit-')) {
|
|
try {
|
|
const session = await this.loadSession(dir);
|
|
if (session.status === 'active') {
|
|
sessions.push(session);
|
|
}
|
|
} catch (e) {
|
|
// Skip invalid sessions
|
|
}
|
|
}
|
|
}
|
|
|
|
return sessions;
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async processElicitationMessage(message) {
|
|
const { sessionId, action, data } = message.data;
|
|
|
|
switch (action) {
|
|
case 'create':
|
|
return await this.createSession(data.agent, data.context);
|
|
|
|
case 'question':
|
|
return await this.addQuestion(sessionId, data.question, data.metadata);
|
|
|
|
case 'response':
|
|
return await this.addResponse(sessionId, data.response, data.questionId);
|
|
|
|
case 'complete':
|
|
return await this.completeSession(sessionId, data.result);
|
|
|
|
case 'update_phase':
|
|
return await this.updatePhase(sessionId, data.phase);
|
|
|
|
default:
|
|
throw new Error(`Unknown elicitation action: ${action}`);
|
|
}
|
|
}
|
|
|
|
async formatElicitationPrompt(session, question) {
|
|
const history = session.context.elicitationHistory
|
|
.filter(h => h.type === 'question' || h.type === 'response')
|
|
.slice(-6); // Last 3 Q&A pairs for context
|
|
|
|
let prompt = `## BMAD ${session.agent} - Elicitation\n\n`;
|
|
|
|
if (history.length > 0) {
|
|
prompt += `### Previous Context:\n`;
|
|
for (const entry of history) {
|
|
if (entry.type === 'question') {
|
|
prompt += `**Q**: ${entry.text}\n`;
|
|
} else {
|
|
prompt += `**A**: ${entry.text}\n\n`;
|
|
}
|
|
}
|
|
}
|
|
|
|
prompt += `### Current Question:\n${question}\n\n`;
|
|
prompt += `*Please provide your response to continue the ${session.agent} workflow.*`;
|
|
|
|
return prompt;
|
|
}
|
|
|
|
async getSessionSummary(sessionId) {
|
|
const session = await this.loadSession(sessionId);
|
|
|
|
const questions = session.context.elicitationHistory.filter(h => h.type === 'question');
|
|
const responses = session.context.elicitationHistory.filter(h => h.type === 'response');
|
|
|
|
return {
|
|
id: session.id,
|
|
agent: session.agent,
|
|
status: session.status,
|
|
created: session.created,
|
|
questionsAsked: questions.length,
|
|
responsesReceived: responses.length,
|
|
currentPhase: session.currentPhase,
|
|
duration: session.metadata.duration || (Date.now() - session.metadata.startTime),
|
|
lastActivity: Math.max(
|
|
...session.context.elicitationHistory.map(h => new Date(h.timestamp).getTime())
|
|
)
|
|
};
|
|
}
|
|
}
|
|
|
|
// CLI interface for testing
|
|
if (require.main === module) {
|
|
const BMADMessageQueue = require('./message-queue');
|
|
const queue = new BMADMessageQueue();
|
|
const broker = new ElicitationBroker(queue);
|
|
|
|
const commands = {
|
|
async create(agent, context = '{}') {
|
|
const session = await broker.createSession(agent, JSON.parse(context));
|
|
console.log(`Session created: ${session.id}`);
|
|
console.log(JSON.stringify(session, null, 2));
|
|
},
|
|
|
|
async question(sessionId, questionText) {
|
|
const question = await broker.addQuestion(sessionId, questionText);
|
|
console.log('Question added:', question);
|
|
},
|
|
|
|
async response(sessionId, responseText) {
|
|
const response = await broker.addResponse(sessionId, responseText);
|
|
console.log('Response added:', response);
|
|
},
|
|
|
|
async complete(sessionId) {
|
|
const session = await broker.completeSession(sessionId);
|
|
console.log('Session completed:', session.id);
|
|
},
|
|
|
|
async active() {
|
|
const sessions = await broker.getActiveElicitations();
|
|
console.log(`Active elicitation sessions (${sessions.length}):`);
|
|
for (const session of sessions) {
|
|
const summary = await broker.getSessionSummary(session.id);
|
|
console.log(` ${summary.id} - ${summary.agent} - Q:${summary.questionsAsked} R:${summary.responsesReceived}`);
|
|
}
|
|
},
|
|
|
|
async summary(sessionId) {
|
|
const summary = await broker.getSessionSummary(sessionId);
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
}
|
|
};
|
|
|
|
const [,, command, ...args] = process.argv;
|
|
|
|
if (commands[command]) {
|
|
commands[command](...args).catch(console.error);
|
|
} else {
|
|
console.log('Usage: elicitation-broker.js <command> [args]');
|
|
console.log('Commands:', Object.keys(commands).join(', '));
|
|
}
|
|
}
|
|
|
|
module.exports = ElicitationBroker; |