anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
675 lines • 25.3 kB
JavaScript
"use strict";
/**
* Conversation Manager for MCP
*
* Orchestrates conversation flow between agents and LLMs with context management
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConversationManager = void 0;
const events_1 = require("events");
const types_1 = require("../types");
/**
* Conversation Manager
*/
class ConversationManager extends events_1.EventEmitter {
constructor(messageRouter, contextManager, agentLLMManager, config = {
autoSummarize: true,
maxIdleTime: 1800000, // 30 minutes
contextSwitchThreshold: 0.7,
multiTurnEnabled: true,
persistHistory: true,
enableStreaming: true
}) {
super();
this.messageRouter = messageRouter;
this.contextManager = contextManager;
this.agentLLMManager = agentLLMManager;
this.config = config;
this.sessions = new Map();
this.agentSessions = new Map(); // agentDID -> sessionIds
this.multiTurnStates = new Map();
this.startIdleCheck();
this.setupEventHandlers();
}
/**
* Start conversation session
*/
async startConversation(agentDID, options = {}) {
const sessionId = `conv-${agentDID}-${Date.now()}`;
// Create context metadata
const contextMetadata = {
agentName: agentDID.split(':').pop() || 'Unknown Agent',
purpose: options.purpose || 'General conversation',
domain: options.domain || 'general',
priority: options.priority || types_1.ContextPriority.MEDIUM,
retention: {
duration: 24 * 60 * 60 * 1000, // 24 hours default
autoCompress: true,
autoDelete: false,
archiveAfter: 30 * 24 * 60 * 60 * 1000, // 30 days
...options.retention
},
sharedWith: []
};
// Create conversation context
const context = await this.contextManager.createContext(agentDID, sessionId, contextMetadata);
// Add system message if provided
if (options.systemPrompt) {
await this.contextManager.addMessage(context.conversationId, {
role: types_1.MessageRole.SYSTEM,
content: options.systemPrompt,
metadata: {
source: 'conversation-manager',
type: 'system-prompt'
}
});
}
// Create session
const session = {
id: sessionId,
agentDID,
contextId: context.conversationId,
metadata: {
domain: context.metadata.domain,
purpose: context.metadata.purpose,
startedAt: new Date(),
lastActivity: new Date(),
messageCount: 0,
totalTokens: 0
},
status: 'active',
participants: [agentDID]
};
this.sessions.set(sessionId, session);
// Update agent sessions index
const agentSessions = this.agentSessions.get(agentDID) || new Set();
agentSessions.add(sessionId);
this.agentSessions.set(agentDID, agentSessions);
this.emit('conversation_started', session);
return session;
}
/**
* Send message in conversation
*/
async sendMessage(sessionId, content, options = {}) {
const session = this.sessions.get(sessionId);
if (!session || session.status !== 'active') {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_REQUEST,
message: `Invalid or inactive session: ${sessionId}`,
timestamp: new Date(),
retryable: false
});
}
const turnStartTime = Date.now();
const context = this.contextManager.getContext(session.contextId);
if (!context) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_REQUEST,
message: `Context not found: ${session.contextId}`,
timestamp: new Date(),
retryable: false
});
}
try {
// Add user message to context
const userMessage = await this.contextManager.addMessage(context.conversationId, {
role: options.role || types_1.MessageRole.USER,
content,
metadata: {
...options.metadata,
source: 'user',
sessionId
}
});
// Create LLM request
const request = {
id: `req-${sessionId}-${Date.now()}`,
type: options.enableFunctions ? types_1.LLMRequestType.FUNCTION_CALL : types_1.LLMRequestType.COMPLETION,
prompt: await this.buildPrompt(context),
agentDID: session.agentDID,
sessionId,
parameters: {
temperature: options.temperature,
maxTokens: options.maxTokens,
model: undefined // Let provider selection handle this
},
metadata: {
agentDID: session.agentDID,
sessionId,
requestId: `req-${sessionId}-${Date.now()}`,
timestamp: new Date(),
source: 'conversation-manager',
priority: options.priority || types_1.RequestPriority.MEDIUM,
conversationId: context.conversationId,
providerId: options.providerId
}
};
// Handle function definitions if enabled
if (options.enableFunctions) {
request.functions = await this.getFunctionDefinitions(session.agentDID);
}
let response;
let functionCalls = [];
let functionResults = [];
// Route request
if (options.streamResponse && this.config.enableStreaming) {
response = await this.handleStreamingRequest(request, session, context);
}
else {
response = await this.messageRouter.routeMessage(request);
}
// Handle function calls
if (response.functionCall) {
functionCalls = [response.functionCall];
// Execute function if handler available
const result = await this.agentLLMManager.executeFunctionCall(response.functionCall);
functionResults = [{
functionCallId: response.functionCall.id || `func-${Date.now()}`,
result,
executionTime: 0, // Will be calculated by the function handler
timestamp: new Date()
}];
// Add function call message
await this.contextManager.addMessage(context.conversationId, {
role: types_1.MessageRole.ASSISTANT,
content: response.content || '',
functionCall: response.functionCall,
metadata: {
source: 'llm',
provider: response.provider,
model: response.model
}
});
// Add function result message
await this.contextManager.addMessage(context.conversationId, {
role: types_1.MessageRole.FUNCTION,
content: JSON.stringify(result),
functionResult: functionResults[0],
metadata: {
source: 'function',
functionName: response.functionCall.name
}
});
// Handle multi-turn if enabled
if (this.config.multiTurnEnabled) {
const followUpResponse = await this.handleMultiTurn(session, context, response, functionResults[0]);
if (followUpResponse) {
response = followUpResponse;
}
}
}
else {
// Add assistant response to context
await this.contextManager.addMessage(context.conversationId, {
role: types_1.MessageRole.ASSISTANT,
content: response.content || '',
metadata: {
source: 'llm',
provider: response.provider,
model: response.model,
usage: response.usage
}
});
}
// Update session metadata
session.metadata.lastActivity = new Date();
session.metadata.messageCount++;
if (response.usage) {
session.metadata.totalTokens += response.usage.totalTokens || 0;
}
// Check if context switch needed
if (this.shouldSwitchContext(context)) {
await this.handleContextSwitch(session);
}
const turnResult = {
request,
response,
context,
session,
functionCalls,
functionResults,
metrics: {
turnDuration: Date.now() - turnStartTime,
tokensUsed: response.usage?.totalTokens || 0,
providerUsed: response.provider || 'unknown'
}
};
this.emit('turn_completed', turnResult);
return turnResult;
}
catch (error) {
this.emit('turn_error', {
sessionId,
error,
context
});
throw error;
}
}
/**
* Stream message in conversation
*/
async *streamMessage(sessionId, content, options = {}) {
const session = this.sessions.get(sessionId);
if (!session || session.status !== 'active') {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_REQUEST,
message: `Invalid or inactive session: ${sessionId}`,
timestamp: new Date(),
retryable: false
});
}
const context = this.contextManager.getContext(session.contextId);
if (!context) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_REQUEST,
message: `Context not found: ${session.contextId}`,
timestamp: new Date(),
retryable: false
});
}
// Add user message
await this.contextManager.addMessage(context.conversationId, {
role: options.role || types_1.MessageRole.USER,
content,
metadata: {
...options.metadata,
source: 'user',
sessionId
}
});
// Create request
const request = {
id: `stream-${sessionId}-${Date.now()}`,
type: types_1.LLMRequestType.COMPLETION,
prompt: await this.buildPrompt(context),
agentDID: session.agentDID,
sessionId,
parameters: {
temperature: options.temperature,
maxTokens: options.maxTokens,
stream: true
},
metadata: {
agentDID: session.agentDID,
sessionId,
requestId: `stream-${sessionId}-${Date.now()}`,
timestamp: new Date(),
source: 'conversation-manager',
priority: options.priority || types_1.RequestPriority.MEDIUM,
conversationId: context.conversationId
}
};
let fullContent = '';
let totalTokens = 0;
try {
for await (const chunk of this.messageRouter.streamMessage(request)) {
fullContent += chunk.content || '';
totalTokens += chunk.tokens || 0;
yield chunk;
}
// Add complete response to context
await this.contextManager.addMessage(context.conversationId, {
role: types_1.MessageRole.ASSISTANT,
content: fullContent,
metadata: {
source: 'llm',
streaming: true,
totalTokens
}
});
// Update session
session.metadata.lastActivity = new Date();
session.metadata.messageCount++;
session.metadata.totalTokens += totalTokens;
}
catch (error) {
this.emit('stream_error', {
sessionId,
error,
partialContent: fullContent
});
throw error;
}
}
/**
* Handle streaming request
*/
async handleStreamingRequest(request, session, context) {
let fullContent = '';
let totalTokens = 0;
let provider = '';
let model = '';
for await (const chunk of this.messageRouter.streamMessage(request)) {
fullContent += chunk.content || '';
totalTokens += chunk.tokens || 0;
provider = chunk.provider || provider;
model = chunk.model || model;
// Emit streaming chunk
this.emit('streaming_chunk', {
sessionId: session.id,
chunk
});
}
return {
id: request.id,
status: 'success',
content: fullContent,
provider,
model,
timestamp: new Date(),
usage: {
totalTokens,
promptTokens: 0,
completionTokens: totalTokens,
model,
provider
}
};
}
/**
* Handle multi-turn conversation
*/
async handleMultiTurn(session, context, lastResponse, functionResult) {
const state = this.multiTurnStates.get(session.id) || {
sessionId: session.id,
pendingFunctions: [],
awaitingInput: false,
turnCount: 0
};
state.turnCount++;
state.lastResponse = lastResponse;
// Check if we need to continue the conversation
const needsContinuation = this.shouldContinueMultiTurn(lastResponse, functionResult);
if (needsContinuation) {
// Build follow-up request
const followUpRequest = {
id: `followup-${session.id}-${Date.now()}`,
type: types_1.LLMRequestType.COMPLETION,
prompt: await this.buildPrompt(context),
agentDID: session.agentDID,
sessionId: session.id,
parameters: {
temperature: 0.7,
maxTokens: 500
},
metadata: {
agentDID: session.agentDID,
sessionId: session.id,
requestId: `followup-${session.id}-${Date.now()}`,
timestamp: new Date(),
source: 'multi-turn',
priority: types_1.RequestPriority.MEDIUM,
conversationId: context.conversationId,
multiTurn: true
}
};
try {
const response = await this.messageRouter.routeMessage(followUpRequest);
// Add follow-up response to context
await this.contextManager.addMessage(context.conversationId, {
role: types_1.MessageRole.ASSISTANT,
content: response.content || '',
metadata: {
source: 'llm-followup',
provider: response.provider,
model: response.model,
multiTurn: true
}
});
this.multiTurnStates.set(session.id, state);
return response;
}
catch (error) {
// Clean up state on error
this.multiTurnStates.delete(session.id);
throw error;
}
}
else {
// Clean up multi-turn state
this.multiTurnStates.delete(session.id);
return null;
}
}
/**
* Check if multi-turn should continue
*/
shouldContinueMultiTurn(lastResponse, functionResult) {
// Continue if function result indicates more work needed
if (functionResult.result?.continue === true) {
return true;
}
// Continue if response asks for more information
const content = lastResponse.content?.toLowerCase() || '';
if (content.includes('need more') ||
content.includes('additional information') ||
content.includes('clarification')) {
return true;
}
// Continue if response is incomplete
if (content.endsWith('...') ||
content.includes('let me continue') ||
content.includes('furthermore')) {
return true;
}
return false;
}
/**
* Build prompt from context
*/
async buildPrompt(context) {
let prompt = '';
// Add context summary if available
if (context.summary) {
prompt += `Previous conversation summary:\n${context.summary}\n\n`;
}
// Add recent messages
const recentMessages = context.history.slice(-10); // Last 10 messages
for (const message of recentMessages) {
let roleLabel = message.role;
if (message.role === types_1.MessageRole.ASSISTANT) {
roleLabel = 'Assistant';
}
else if (message.role === types_1.MessageRole.USER) {
roleLabel = 'User';
}
else if (message.role === types_1.MessageRole.SYSTEM) {
roleLabel = 'System';
}
else if (message.role === types_1.MessageRole.FUNCTION) {
roleLabel = `Function (${message.functionCall?.name || 'unknown'})`;
}
prompt += `${roleLabel}: ${message.content}\n`;
if (message.functionCall) {
prompt += `Function Call: ${message.functionCall.name}(${JSON.stringify(message.functionCall.arguments)})\n`;
}
}
return prompt.trim();
}
/**
* Get function definitions for agent
*/
async getFunctionDefinitions(agentDID) {
// This would integrate with the agent's available functions
// For now, return empty array
return [];
}
/**
* Check if context should be switched
*/
shouldSwitchContext(context) {
const tokenRatio = context.tokens / context.maxTokens;
return tokenRatio > this.config.contextSwitchThreshold;
}
/**
* Handle context switch
*/
async handleContextSwitch(session) {
const oldContext = this.contextManager.getContext(session.contextId);
if (!oldContext)
return;
// Compress old context
await this.contextManager.compressContext(oldContext);
// Create new context
const newContext = await this.contextManager.createContext(session.agentDID, `${session.id}-continued`, {
...oldContext.metadata,
purpose: `Continued from ${session.contextId}`
});
// Add summary message
if (oldContext.summary) {
await this.contextManager.addMessage(newContext.conversationId, {
role: types_1.MessageRole.SYSTEM,
content: `Previous conversation summary: ${oldContext.summary}`,
metadata: {
source: 'context-switch',
previousContextId: oldContext.conversationId
}
});
}
// Update session
session.contextId = newContext.conversationId;
this.emit('context_switched', {
session,
oldContextId: oldContext.conversationId,
newContextId: newContext.conversationId
});
}
/**
* Pause conversation
*/
pauseConversation(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.status = 'paused';
this.emit('conversation_paused', session);
}
}
/**
* Resume conversation
*/
resumeConversation(sessionId) {
const session = this.sessions.get(sessionId);
if (session && session.status === 'paused') {
session.status = 'active';
session.metadata.lastActivity = new Date();
this.emit('conversation_resumed', session);
}
}
/**
* End conversation
*/
async endConversation(sessionId) {
const session = this.sessions.get(sessionId);
if (!session)
return;
session.status = 'ended';
session.metadata.lastActivity = new Date();
// Clean up multi-turn state
this.multiTurnStates.delete(sessionId);
// Remove from agent sessions
const agentSessions = this.agentSessions.get(session.agentDID);
if (agentSessions) {
agentSessions.delete(sessionId);
if (agentSessions.size === 0) {
this.agentSessions.delete(session.agentDID);
}
}
// Auto-summarize if enabled
if (this.config.autoSummarize) {
const context = this.contextManager.getContext(session.contextId);
if (context && context.history.length > 5) {
await this.contextManager.compressContext(context);
}
}
this.emit('conversation_ended', session);
}
/**
* Get agent conversations
*/
getAgentConversations(agentDID) {
const sessionIds = this.agentSessions.get(agentDID) || new Set();
const sessions = Array.from(sessionIds)
.map(id => this.sessions.get(id))
.filter(Boolean);
return sessions.sort((a, b) => b.metadata.lastActivity.getTime() - a.metadata.lastActivity.getTime());
}
/**
* Get conversation statistics
*/
getStatistics() {
const sessions = Array.from(this.sessions.values());
return {
totalSessions: sessions.length,
activeSessions: sessions.filter(s => s.status === 'active').length,
pausedSessions: sessions.filter(s => s.status === 'paused').length,
endedSessions: sessions.filter(s => s.status === 'ended').length,
averageMessagesPerSession: sessions.reduce((sum, s) => sum + s.metadata.messageCount, 0) /
(sessions.length || 1),
averageTokensPerSession: sessions.reduce((sum, s) => sum + s.metadata.totalTokens, 0) /
(sessions.length || 1),
multiTurnSessions: this.multiTurnStates.size
};
}
/**
* Setup event handlers
*/
setupEventHandlers() {
this.messageRouter.on('provider_health_updated', (health) => {
this.emit('provider_health_updated', health);
});
this.contextManager.on('context_compressed', (event) => {
this.emit('context_compressed', event);
});
this.agentLLMManager.on('function_executed', (event) => {
this.emit('function_executed', event);
});
}
/**
* Start idle session check
*/
startIdleCheck() {
this.idleCheckTimer = setInterval(() => {
this.checkIdleSessions();
}, 60000); // Check every minute
}
/**
* Check for idle sessions
*/
checkIdleSessions() {
const now = Date.now();
const idleSessions = [];
for (const [id, session] of this.sessions) {
const idleTime = now - session.metadata.lastActivity.getTime();
if (idleTime > this.config.maxIdleTime && session.status === 'active') {
this.pauseConversation(id);
idleSessions.push(id);
}
}
if (idleSessions.length > 0) {
this.emit('sessions_idle', idleSessions);
}
}
/**
* Shutdown
*/
shutdown() {
if (this.idleCheckTimer) {
clearInterval(this.idleCheckTimer);
}
// End all active sessions
for (const [sessionId, session] of this.sessions) {
if (session.status === 'active') {
this.endConversation(sessionId);
}
}
this.sessions.clear();
this.agentSessions.clear();
this.multiTurnStates.clear();
this.removeAllListeners();
}
}
exports.ConversationManager = ConversationManager;
exports.default = ConversationManager;
//# sourceMappingURL=conversation-manager.js.map