anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
666 lines • 25.2 kB
JavaScript
"use strict";
/**
* Context Manager for MCP
*
* Manages conversation contexts across agent-LLM interactions with compression and sharing
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextManager = void 0;
const events_1 = require("events");
const types_1 = require("../types");
const memory_storage_provider_1 = require("../../storage/providers/memory-storage-provider");
/**
* Context Manager
*/
class ContextManager extends events_1.EventEmitter {
constructor(config = {
maxTokensPerContext: 4000,
compressionThreshold: 0.8, // Compress at 80% capacity
compressionStrategy: 'importance',
retentionCheckInterval: 3600000, // 1 hour
sharing: {
allowSharing: true,
requireConsent: true,
maxShareDepth: 2,
shareableFields: ['domain', 'purpose', 'summary']
},
archiveAfterDays: 30
}, storageProvider) {
super();
this.config = config;
this.contexts = new Map();
this.sessionIndex = new Map(); // sessionId -> contextIds
this.agentIndex = new Map(); // agentDID -> contextIds
this.domainIndex = new Map(); // domain -> contextIds
this.archivedContexts = new Map();
this.storageProvider = storageProvider || new memory_storage_provider_1.MemoryStorageProvider();
this.loadContexts();
this.startMaintenanceTasks();
}
/**
* Create or update context
*/
async createContext(agentDID, sessionId, metadata) {
const conversationId = `conv-${agentDID}-${sessionId}-${Date.now()}`;
const context = {
agentDID,
sessionId,
conversationId,
history: [],
metadata,
lastUpdated: new Date(),
tokens: 0,
maxTokens: this.config.maxTokensPerContext
};
this.contexts.set(conversationId, context);
this.updateIndices(context);
await this.saveContexts();
this.emit('context_created', context);
return context;
}
/**
* Get context
*/
getContext(conversationId) {
return this.contexts.get(conversationId) || this.archivedContexts.get(conversationId) || null;
}
/**
* Get contexts for agent
*/
getAgentContexts(agentDID, includeArchived = false) {
const contextIds = this.agentIndex.get(agentDID) || new Set();
const contexts = Array.from(contextIds)
.map(id => this.contexts.get(id))
.filter(Boolean);
if (includeArchived) {
const archivedIds = Array.from(this.archivedContexts.values())
.filter(ctx => ctx.agentDID === agentDID);
contexts.push(...archivedIds);
}
return contexts.sort((a, b) => b.lastUpdated.getTime() - a.lastUpdated.getTime());
}
/**
* Add message to context
*/
async addMessage(conversationId, message) {
const context = this.contexts.get(conversationId);
if (!context) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_REQUEST,
message: `Context not found: ${conversationId}`,
timestamp: new Date(),
retryable: false
});
}
const fullMessage = {
...message,
id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date()
};
context.history.push(fullMessage);
context.lastUpdated = new Date();
// Update token count
context.tokens += this.estimateTokens(fullMessage.content);
// Check if compression needed
if (context.tokens > context.maxTokens * this.config.compressionThreshold) {
await this.compressContext(context);
}
await this.saveContexts();
this.emit('message_added', { context, message: fullMessage });
return fullMessage;
}
/**
* Compress context
*/
async compressContext(context) {
const originalTokens = context.tokens;
let compressedTokens = 0;
let droppedMessages = 0;
let summary = '';
switch (this.config.compressionStrategy) {
case 'summary':
const result = await this.summarizeContext(context);
summary = result.summary;
compressedTokens = result.tokens;
droppedMessages = result.droppedMessages;
break;
case 'sliding-window':
const windowResult = this.applySlidingWindow(context);
compressedTokens = windowResult.tokens;
droppedMessages = windowResult.droppedMessages;
break;
case 'importance':
const importanceResult = await this.compressByImportance(context);
summary = importanceResult.summary;
compressedTokens = importanceResult.tokens;
droppedMessages = importanceResult.droppedMessages;
break;
}
context.tokens = compressedTokens;
context.compressedAt = new Date();
if (summary) {
context.summary = summary;
}
await this.saveContexts();
const compressionResult = {
originalTokens,
compressedTokens,
compressionRatio: compressedTokens / originalTokens,
summary,
droppedMessages
};
this.emit('context_compressed', { context, result: compressionResult });
return compressionResult;
}
/**
* Summarize context
*/
async summarizeContext(context) {
// Group messages by topic/function
const topics = this.groupMessagesByTopic(context.history);
// Create summary of key points
const summaryParts = [];
let keptMessages = [];
for (const [topic, messages] of topics) {
// Keep most recent message per topic
const recent = messages[messages.length - 1];
keptMessages.push(recent);
// Add to summary
if (messages.length > 1) {
summaryParts.push(`${topic}: ${messages.length} messages, latest: ${recent.content.substring(0, 100)}...`);
}
}
// Keep system messages and function calls
const importantMessages = context.history.filter(msg => msg.role === types_1.MessageRole.SYSTEM ||
msg.functionCall ||
msg.functionResult);
keptMessages.push(...importantMessages);
// Sort by timestamp
keptMessages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
// Update context
const droppedMessages = context.history.length - keptMessages.length;
context.history = keptMessages;
const summary = summaryParts.join('\n');
const tokens = keptMessages.reduce((sum, msg) => sum + this.estimateTokens(msg.content), 0);
return { summary, tokens, droppedMessages };
}
/**
* Apply sliding window compression
*/
applySlidingWindow(context) {
const targetTokens = context.maxTokens * 0.6; // Keep 60% after compression
let currentTokens = context.tokens;
let droppedMessages = 0;
// Remove oldest messages until under target
while (currentTokens > targetTokens && context.history.length > 10) {
const removed = context.history.shift();
if (removed) {
currentTokens -= this.estimateTokens(removed.content);
droppedMessages++;
}
}
return { tokens: currentTokens, droppedMessages };
}
/**
* Compress by importance
*/
async compressByImportance(context) {
// Score each message by importance
const scoredMessages = context.history.map(msg => ({
message: msg,
score: this.calculateMessageImportance(msg, context)
}));
// Sort by importance
scoredMessages.sort((a, b) => b.score - a.score);
// Keep top messages within token limit
const targetTokens = context.maxTokens * 0.7;
let currentTokens = 0;
const keptMessages = [];
const droppedSummary = [];
for (const { message, score } of scoredMessages) {
const messageTokens = this.estimateTokens(message.content);
if (currentTokens + messageTokens <= targetTokens || score > 0.8) {
keptMessages.push(message);
currentTokens += messageTokens;
}
else {
droppedSummary.push(`[${message.role}]: ${message.content.substring(0, 50)}...`);
}
}
// Sort kept messages by timestamp
keptMessages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
const droppedMessages = context.history.length - keptMessages.length;
context.history = keptMessages;
const summary = droppedMessages > 0
? `Compressed ${droppedMessages} messages:\n${droppedSummary.slice(0, 5).join('\n')}`
: '';
return { summary, tokens: currentTokens, droppedMessages };
}
/**
* Calculate message importance
*/
calculateMessageImportance(message, context) {
let score = 0.5; // Base score
// Role importance
if (message.role === types_1.MessageRole.SYSTEM)
score += 0.3;
if (message.role === types_1.MessageRole.FUNCTION)
score += 0.2;
// Function calls are important
if (message.functionCall)
score += 0.3;
if (message.functionResult)
score += 0.2;
// Recent messages are more important
const age = Date.now() - message.timestamp.getTime();
const ageHours = age / (1000 * 60 * 60);
score += Math.max(0, 0.2 - ageHours / 24);
// Messages with errors or warnings
if (message.content.toLowerCase().includes('error'))
score += 0.2;
if (message.content.toLowerCase().includes('warning'))
score += 0.1;
// Long messages might contain more information
const length = message.content.length;
if (length > 500)
score += 0.1;
// Priority context messages
if (context.metadata.priority === types_1.ContextPriority.CRITICAL)
score += 0.2;
if (context.metadata.priority === types_1.ContextPriority.HIGH)
score += 0.1;
return Math.min(1, score);
}
/**
* Share context with another agent
*/
async shareContext(conversationId, targetAgentDID, options = {}) {
if (!this.config.sharing.allowSharing) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.FORBIDDEN,
message: 'Context sharing is disabled',
timestamp: new Date(),
retryable: false
});
}
const sourceContext = this.contexts.get(conversationId);
if (!sourceContext) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_REQUEST,
message: `Context not found: ${conversationId}`,
timestamp: new Date(),
retryable: false
});
}
// Check if already shared with target
if (sourceContext.metadata.sharedWith.includes(targetAgentDID)) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.INVALID_REQUEST,
message: 'Context already shared with target agent',
timestamp: new Date(),
retryable: false
});
}
// Create shared context
const sharedMetadata = {
...sourceContext.metadata,
sharedWith: [sourceContext.agentDID], // Original owner
agentName: `Shared from ${sourceContext.metadata.agentName}`
};
// Filter metadata based on shareable fields
if (options.shareMetadata !== false) {
for (const key of Object.keys(sharedMetadata)) {
if (!this.config.sharing.shareableFields.includes(key)) {
delete sharedMetadata[key];
}
}
}
const sharedContext = await this.createContext(targetAgentDID, `shared-${sourceContext.sessionId}`, sharedMetadata);
// Copy history if requested
if (options.shareHistory !== false) {
const historyToShare = sourceContext.history.slice(-20); // Last 20 messages
sharedContext.history = historyToShare.map(msg => ({
...msg,
metadata: {
...msg.metadata,
sharedFrom: sourceContext.agentDID
}
}));
}
// Copy summary if available
if (options.shareSummary !== false && sourceContext.summary) {
sharedContext.summary = sourceContext.summary;
}
// Update source context
sourceContext.metadata.sharedWith.push(targetAgentDID);
await this.saveContexts();
this.emit('context_shared', {
source: sourceContext,
target: sharedContext,
sharedWith: targetAgentDID
});
return sharedContext;
}
/**
* Search contexts
*/
async searchContexts(options) {
let contexts = Array.from(this.contexts.values());
if (options.includeArchived) {
contexts.push(...Array.from(this.archivedContexts.values()));
}
// Apply filters
if (options.agentDID) {
contexts = contexts.filter(ctx => ctx.agentDID === options.agentDID);
}
if (options.sessionId) {
contexts = contexts.filter(ctx => ctx.sessionId === options.sessionId);
}
if (options.conversationId) {
contexts = contexts.filter(ctx => ctx.conversationId === options.conversationId);
}
if (options.priority) {
contexts = contexts.filter(ctx => ctx.metadata.priority === options.priority);
}
if (options.domain) {
contexts = contexts.filter(ctx => ctx.metadata.domain === options.domain);
}
// Sort by last updated
contexts.sort((a, b) => b.lastUpdated.getTime() - a.lastUpdated.getTime());
// Apply limit
if (options.limit) {
contexts = contexts.slice(0, options.limit);
}
return contexts;
}
/**
* Archive old contexts
*/
async archiveOldContexts() {
const archiveDate = new Date(Date.now() - this.config.archiveAfterDays * 24 * 60 * 60 * 1000);
const toArchive = [];
for (const [id, context] of this.contexts) {
if (context.lastUpdated < archiveDate &&
context.metadata.retention.autoDelete !== false) {
toArchive.push(context);
}
}
for (const context of toArchive) {
this.contexts.delete(context.conversationId);
this.archivedContexts.set(context.conversationId, context);
// Update indices
this.removeFromIndices(context);
this.emit('context_archived', context);
}
if (toArchive.length > 0) {
await this.saveContexts();
}
}
/**
* Apply retention policies
*/
async applyRetentionPolicies() {
const now = Date.now();
const toDelete = [];
// Check active contexts
for (const [id, context] of this.contexts) {
const retention = context.metadata.retention;
if (retention.duration &&
now - context.lastUpdated.getTime() > retention.duration) {
if (retention.autoDelete) {
toDelete.push(id);
}
else if (retention.archiveAfter) {
// Move to archive instead
this.archivedContexts.set(id, context);
this.contexts.delete(id);
}
}
}
// Check archived contexts
for (const [id, context] of this.archivedContexts) {
const retention = context.metadata.retention;
if (retention.autoDelete && retention.duration &&
now - context.lastUpdated.getTime() > retention.duration * 2) {
this.archivedContexts.delete(id);
this.emit('context_deleted', context);
}
}
// Delete contexts
for (const id of toDelete) {
const context = this.contexts.get(id);
if (context) {
this.contexts.delete(id);
this.removeFromIndices(context);
this.emit('context_deleted', context);
}
}
if (toDelete.length > 0) {
await this.saveContexts();
}
}
/**
* Group messages by topic
*/
groupMessagesByTopic(messages) {
const topics = new Map();
for (const message of messages) {
let topic = 'general';
// Determine topic based on content
if (message.functionCall) {
topic = `function:${message.functionCall.name}`;
}
else if (message.content.includes('error')) {
topic = 'errors';
}
else if (message.content.includes('delegation')) {
topic = 'delegation';
}
else if (message.metadata?.topic) {
topic = message.metadata.topic;
}
const topicMessages = topics.get(topic) || [];
topicMessages.push(message);
topics.set(topic, topicMessages);
}
return topics;
}
/**
* Estimate token count
*/
estimateTokens(text) {
// Rough estimate: 1 token per 4 characters
return Math.ceil(text.length / 4);
}
/**
* Update indices
*/
updateIndices(context) {
// Session index
const sessionContexts = this.sessionIndex.get(context.sessionId) || new Set();
sessionContexts.add(context.conversationId);
this.sessionIndex.set(context.sessionId, sessionContexts);
// Agent index
const agentContexts = this.agentIndex.get(context.agentDID) || new Set();
agentContexts.add(context.conversationId);
this.agentIndex.set(context.agentDID, agentContexts);
// Domain index
const domainContexts = this.domainIndex.get(context.metadata.domain) || new Set();
domainContexts.add(context.conversationId);
this.domainIndex.set(context.metadata.domain, domainContexts);
}
/**
* Remove from indices
*/
removeFromIndices(context) {
this.sessionIndex.get(context.sessionId)?.delete(context.conversationId);
this.agentIndex.get(context.agentDID)?.delete(context.conversationId);
this.domainIndex.get(context.metadata.domain)?.delete(context.conversationId);
}
/**
* Load contexts from storage
*/
async loadContexts() {
try {
const storage = this.storageProvider._storage;
if (!storage)
return;
const storedContexts = storage.get('mcp:contexts');
if (storedContexts) {
const contexts = JSON.parse(storedContexts);
for (const context of contexts) {
// Convert dates
context.lastUpdated = new Date(context.lastUpdated);
if (context.compressedAt) {
context.compressedAt = new Date(context.compressedAt);
}
context.history.forEach((msg) => {
msg.timestamp = new Date(msg.timestamp);
});
this.contexts.set(context.conversationId, context);
this.updateIndices(context);
}
}
const storedArchived = storage.get('mcp:contexts:archived');
if (storedArchived) {
const archived = JSON.parse(storedArchived);
for (const context of archived) {
// Convert dates
context.lastUpdated = new Date(context.lastUpdated);
if (context.compressedAt) {
context.compressedAt = new Date(context.compressedAt);
}
context.history.forEach((msg) => {
msg.timestamp = new Date(msg.timestamp);
});
this.archivedContexts.set(context.conversationId, context);
}
}
}
catch (error) {
this.emit('error', error);
}
}
/**
* Save contexts to storage
*/
async saveContexts() {
try {
const storage = this.storageProvider._storage || new Map();
storage.set('mcp:contexts', JSON.stringify(Array.from(this.contexts.values())));
storage.set('mcp:contexts:archived', JSON.stringify(Array.from(this.archivedContexts.values())));
this.storageProvider._storage = storage;
}
catch (error) {
this.emit('error', error);
}
}
/**
* Start maintenance tasks
*/
startMaintenanceTasks() {
// Compression check every 5 minutes
this.compressionTimer = setInterval(() => {
this.checkCompressionNeeded();
}, 5 * 60 * 1000);
// Retention check
this.retentionTimer = setInterval(() => {
this.applyRetentionPolicies();
this.archiveOldContexts();
}, this.config.retentionCheckInterval);
}
/**
* Check if compression needed
*/
async checkCompressionNeeded() {
for (const [, context] of this.contexts) {
if (context.tokens > context.maxTokens * this.config.compressionThreshold) {
await this.compressContext(context);
}
}
}
/**
* Get statistics
*/
getStatistics() {
const stats = {
totalContexts: this.contexts.size + this.archivedContexts.size,
activeContexts: this.contexts.size,
archivedContexts: this.archivedContexts.size,
totalTokens: 0,
averageTokensPerContext: 0,
compressionsSaved: 0,
contextsByPriority: {
[types_1.ContextPriority.LOW]: 0,
[types_1.ContextPriority.MEDIUM]: 0,
[types_1.ContextPriority.HIGH]: 0,
[types_1.ContextPriority.CRITICAL]: 0
},
contextsByDomain: {}
};
let totalCompressed = 0;
for (const context of this.contexts.values()) {
stats.totalTokens += context.tokens;
stats.contextsByPriority[context.metadata.priority]++;
stats.contextsByDomain[context.metadata.domain] =
(stats.contextsByDomain[context.metadata.domain] || 0) + 1;
if (context.compressedAt) {
totalCompressed++;
}
}
stats.averageTokensPerContext = stats.activeContexts > 0
? stats.totalTokens / stats.activeContexts
: 0;
stats.compressionsSaved = totalCompressed;
return stats;
}
/**
* Clear context
*/
async clearContext(conversationId) {
const context = this.contexts.get(conversationId);
if (context) {
context.history = [];
context.tokens = 0;
context.summary = undefined;
context.compressedAt = undefined;
context.lastUpdated = new Date();
await this.saveContexts();
this.emit('context_cleared', context);
}
}
/**
* Delete context
*/
async deleteContext(conversationId) {
const context = this.contexts.get(conversationId) || this.archivedContexts.get(conversationId);
if (context) {
this.contexts.delete(conversationId);
this.archivedContexts.delete(conversationId);
this.removeFromIndices(context);
await this.saveContexts();
this.emit('context_deleted', context);
}
}
/**
* Shutdown
*/
shutdown() {
if (this.compressionTimer) {
clearInterval(this.compressionTimer);
}
if (this.retentionTimer) {
clearInterval(this.retentionTimer);
}
this.saveContexts().catch(() => { });
this.contexts.clear();
this.archivedContexts.clear();
this.sessionIndex.clear();
this.agentIndex.clear();
this.domainIndex.clear();
this.removeAllListeners();
}
}
exports.ContextManager = ContextManager;
exports.default = ContextManager;
//# sourceMappingURL=context-manager.js.map