import { z } from 'zod';
import { createLogger } from '../utils/logger.js';
/**
* Cross-Tool Memory Service
*
* Preserves context across tool invocations, learns from interactions,
* and provides intelligent context for subsequent queries.
*/
// Memory schemas
export const QueryContextSchema = z.object({
id: z.string(),
timestamp: z.string().datetime(),
tool: z.string(),
input: z.any(),
output: z.any(),
insights: z.array(z.string()).optional(),
metadata: z.object({
execution_time_ms: z.number(),
confidence_score: z.number().optional(),
data_quality: z.string().optional()
}).optional()
});
export const ConversationThreadSchema = z.object({
thread_id: z.string(),
created_at: z.string().datetime(),
last_active: z.string().datetime(),
queries: z.array(QueryContextSchema),
context: z.object({
domain: z.string().optional(),
organization_id: z.string().optional(),
project_ids: z.array(z.string()).optional(),
key_metrics: z.record(z.any()).optional(),
learned_preferences: z.record(z.any()).optional()
}),
summary: z.string().optional()
});
export const MemoryStoreSchema = z.object({
threads: z.map(z.string(), ConversationThreadSchema),
global_insights: z.array(z.object({
insight: z.string(),
confidence: z.number(),
source_queries: z.array(z.string()),
timestamp: z.string().datetime()
})),
domain_knowledge: z.record(z.object({
facts: z.array(z.string()),
patterns: z.array(z.string()),
benchmarks: z.record(z.any())
}))
});
export type QueryContext = z.infer<typeof QueryContextSchema>;
export type ConversationThread = z.infer<typeof ConversationThreadSchema>;
export type MemoryStore = z.infer<typeof MemoryStoreSchema>;
export class CrossToolMemory {
private logger = createLogger({ component: 'CrossToolMemory' });
private memoryStore: MemoryStore;
private activeThreadId: string | null = null;
// Memory configuration
private readonly CONFIG = {
MAX_QUERIES_PER_THREAD: 50,
THREAD_TIMEOUT_HOURS: 24,
INSIGHT_CONFIDENCE_THRESHOLD: 0.7,
PATTERN_DETECTION_MIN_SAMPLES: 3
};
constructor() {
this.memoryStore = {
threads: new Map(),
global_insights: [],
domain_knowledge: {}
};
}
/**
* Start or continue a conversation thread
*/
async startThread(threadId?: string): Promise<string> {
const id = threadId || this.generateThreadId();
if (!this.memoryStore.threads.has(id)) {
const newThread: ConversationThread = {
thread_id: id,
created_at: new Date().toISOString(),
last_active: new Date().toISOString(),
queries: [],
context: {}
};
this.memoryStore.threads.set(id, newThread);
this.logger.info('Started new conversation thread', { thread_id: id });
}
this.activeThreadId = id;
return id;
}
/**
* Record a query and its results
*/
async recordQuery(
tool: string,
input: any,
output: any,
metadata?: QueryContext['metadata']
): Promise<void> {
if (!this.activeThreadId) {
await this.startThread();
}
const thread = this.memoryStore.threads.get(this.activeThreadId!);
if (!thread) return;
const query: QueryContext = {
id: this.generateQueryId(),
timestamp: new Date().toISOString(),
tool,
input,
output,
metadata
};
// Extract insights from output
query.insights = this.extractInsights(output, tool);
// Add to thread
thread.queries.push(query);
thread.last_active = new Date().toISOString();
// Update context
this.updateThreadContext(thread, query);
// Learn from query
await this.learnFromQuery(query, thread);
// Cleanup old queries if needed
if (thread.queries.length > this.CONFIG.MAX_QUERIES_PER_THREAD) {
thread.queries = thread.queries.slice(-this.CONFIG.MAX_QUERIES_PER_THREAD);
}
this.logger.debug('Recorded query', {
thread_id: this.activeThreadId,
tool,
insights_count: query.insights?.length || 0
});
}
/**
* Get relevant context for a new query
*/
async getRelevantContext(
tool: string,
input: any
): Promise<{
previous_results: any[];
related_insights: string[];
domain_facts: string[];
suggestions: string[];
}> {
const context = {
previous_results: [] as any[],
related_insights: [] as string[],
domain_facts: [] as string[],
suggestions: [] as string[]
};
if (!this.activeThreadId) return context;
const thread = this.memoryStore.threads.get(this.activeThreadId);
if (!thread) return context;
// Get previous results from same tool
const sameTool = thread.queries
.filter(q => q.tool === tool)
.slice(-3);
context.previous_results = sameTool.map(q => ({
input: q.input,
key_outputs: this.extractKeyOutputs(q.output),
timestamp: q.timestamp
}));
// Get related insights
if (input.project_id || input.organization_id) {
context.related_insights = this.findRelatedInsights(
input.project_id || input.organization_id,
thread
);
}
// Get domain facts
const domain = thread.context.domain || this.inferDomain(input);
if (domain && this.memoryStore.domain_knowledge[domain]) {
context.domain_facts = this.memoryStore.domain_knowledge[domain].facts.slice(0, 5);
}
// Generate suggestions
context.suggestions = this.generateSuggestions(tool, input, thread);
return context;
}
/**
* Get cross-tool insights
*/
async getCrossToolInsights(): Promise<{
patterns: Array<{
pattern: string;
tools_involved: string[];
frequency: number;
confidence: number;
}>;
recommendations: string[];
optimization_opportunities: string[];
}> {
const insights = {
patterns: [] as any[],
recommendations: [] as string[],
optimization_opportunities: [] as string[]
};
if (!this.activeThreadId) return insights;
const thread = this.memoryStore.threads.get(this.activeThreadId);
if (!thread || thread.queries.length < this.CONFIG.PATTERN_DETECTION_MIN_SAMPLES) {
return insights;
}
// Detect patterns
insights.patterns = this.detectQueryPatterns(thread);
// Generate recommendations based on patterns
insights.recommendations = this.generateCrossToolRecommendations(
insights.patterns,
thread
);
// Identify optimization opportunities
insights.optimization_opportunities = this.identifyOptimizations(thread);
return insights;
}
/**
* Summarize conversation thread
*/
async summarizeThread(threadId?: string): Promise<{
summary: string;
key_findings: string[];
decisions_made: string[];
next_steps: string[];
}> {
const id = threadId || this.activeThreadId;
if (!id) return this.getEmptySummary();
const thread = this.memoryStore.threads.get(id);
if (!thread) return this.getEmptySummary();
const summary = {
summary: this.generateThreadSummary(thread),
key_findings: this.extractKeyFindings(thread),
decisions_made: this.extractDecisions(thread),
next_steps: this.suggestNextSteps(thread)
};
// Store summary in thread
thread.summary = summary.summary;
return summary;
}
/**
* Export memory for persistence
*/
async exportMemory(): Promise<string> {
const exportData = {
threads: Array.from(this.memoryStore.threads.entries()).map(([id, thread]) => ({
id,
thread
})),
global_insights: this.memoryStore.global_insights,
domain_knowledge: this.memoryStore.domain_knowledge,
export_timestamp: new Date().toISOString()
};
return JSON.stringify(exportData, null, 2);
}
/**
* Import memory from persistence
*/
async importMemory(data: string): Promise<void> {
try {
const importData = JSON.parse(data);
// Restore threads
this.memoryStore.threads = new Map(
importData.threads.map((item: any) => [item.id, item.thread])
);
// Restore insights and knowledge
this.memoryStore.global_insights = importData.global_insights || [];
this.memoryStore.domain_knowledge = importData.domain_knowledge || {};
this.logger.info('Memory imported successfully', {
thread_count: this.memoryStore.threads.size,
insight_count: this.memoryStore.global_insights.length
});
} catch (error) {
this.logger.error('Failed to import memory', error as Error);
throw error;
}
}
// Private helper methods
private generateThreadId(): string {
return `thread_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private generateQueryId(): string {
return `query_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private extractInsights(output: any, tool: string): string[] {
const insights = [];
// Tool-specific insight extraction
switch (tool) {
case 'predict_roi':
if (output.summary?.expected_roi > 150) {
insights.push(`High ROI project: ${output.summary.expected_roi}%`);
}
if (output.summary?.payback_period_months < 12) {
insights.push(`Quick payback: ${output.summary.payback_period_months} months`);
}
break;
case 'compare_projects':
if (output.insights?.best_overall) {
insights.push(`Best project: ${output.insights.best_overall}`);
}
if (output.insights?.synergies?.length > 0) {
insights.push('Synergy opportunities identified between projects');
}
break;
}
// Extract insights from AI-optimized responses
if (output.insights?.primary) {
insights.push(...output.insights.primary.slice(0, 2));
}
return insights;
}
private updateThreadContext(thread: ConversationThread, query: QueryContext): void {
// Update organization/project context
if (query.input.organization_id) {
thread.context.organization_id = query.input.organization_id;
}
if (query.input.project?.project_name) {
if (!thread.context.project_ids) {
thread.context.project_ids = [];
}
if (query.output.project_id) {
thread.context.project_ids.push(query.output.project_id);
}
}
// Update domain
if (query.input.project?.industry) {
thread.context.domain = query.input.project.industry;
}
// Update key metrics
if (!thread.context.key_metrics) {
thread.context.key_metrics = {};
}
if (query.output.summary) {
Object.assign(thread.context.key_metrics, {
last_roi: query.output.summary.expected_roi,
last_payback: query.output.summary.payback_period_months,
last_npv: query.output.summary.net_present_value
});
}
}
private async learnFromQuery(query: QueryContext, thread: ConversationThread): Promise<void> {
// Learn domain patterns
if (thread.context.domain) {
if (!this.memoryStore.domain_knowledge[thread.context.domain]) {
this.memoryStore.domain_knowledge[thread.context.domain] = {
facts: [],
patterns: [],
benchmarks: {}
};
}
const domain = this.memoryStore.domain_knowledge[thread.context.domain];
// Extract facts
if (query.insights && query.insights.length > 0) {
domain.facts.push(...query.insights);
// Keep unique facts
domain.facts = Array.from(new Set(domain.facts)).slice(-50);
}
// Update benchmarks
if (query.output.summary) {
domain.benchmarks.avg_roi = this.updateAverage(
domain.benchmarks.avg_roi,
query.output.summary.expected_roi
);
domain.benchmarks.avg_payback = this.updateAverage(
domain.benchmarks.avg_payback,
query.output.summary.payback_period_months
);
}
}
// Learn global insights
if (query.metadata?.confidence_score &&
query.metadata.confidence_score > this.CONFIG.INSIGHT_CONFIDENCE_THRESHOLD) {
query.insights?.forEach(insight => {
this.memoryStore.global_insights.push({
insight,
confidence: query.metadata!.confidence_score!,
source_queries: [query.id],
timestamp: query.timestamp
});
});
// Keep recent insights
this.memoryStore.global_insights = this.memoryStore.global_insights
.slice(-100);
}
}
private extractKeyOutputs(output: any): any {
// Extract most important fields based on response structure
if (output.executive_summary) {
return {
headline: output.executive_summary.headline,
key_insight: output.executive_summary.key_insight,
primary_metric: output.executive_summary.primary_metric
};
}
if (output.summary) {
return {
roi: output.summary.expected_roi,
payback: output.summary.payback_period_months,
npv: output.summary.net_present_value
};
}
return output;
}
private findRelatedInsights(identifier: string, thread: ConversationThread): string[] {
const insights = [];
// Find insights from queries with same identifier
thread.queries.forEach(query => {
if (query.input.project_id === identifier ||
query.input.organization_id === identifier) {
insights.push(...(query.insights || []));
}
});
// Add global insights that might be relevant
const relevantGlobal = this.memoryStore.global_insights
.filter(gi => gi.confidence > this.CONFIG.INSIGHT_CONFIDENCE_THRESHOLD)
.map(gi => gi.insight)
.slice(0, 3);
insights.push(...relevantGlobal);
return Array.from(new Set(insights)).slice(0, 5);
}
private inferDomain(input: any): string | null {
if (input.project?.industry) return input.project.industry;
if (input.industry) return input.industry;
// Infer from project type
if (input.project_type?.includes('customer_service')) return 'service';
if (input.project_type?.includes('data_analytics')) return 'technology';
return null;
}
private generateSuggestions(
tool: string,
input: any,
thread: ConversationThread
): string[] {
const suggestions = [];
// Suggest based on previous queries
const previousTools = Array.from(new Set(thread.queries.map(q => q.tool)));
if (tool === 'compare_projects' && thread.context.project_ids?.length === 1) {
suggestions.push('Add more projects for meaningful comparison');
}
// Suggest based on patterns
if (thread.queries.length > 5) {
const lastQueries = thread.queries.slice(-3);
if (lastQueries.every(q => q.tool === tool)) {
suggestions.push('Try a different tool for complementary insights');
}
}
// Suggest based on results
if (input.enable_benchmarks === false) {
suggestions.push('Enable benchmarks for industry comparison');
}
return suggestions;
}
private detectQueryPatterns(thread: ConversationThread): any[] {
const patterns: any[] = [];
const toolSequences = new Map<string, number>();
// Analyze tool usage sequences
for (let i = 0; i < thread.queries.length - 1; i++) {
const sequence = `${thread.queries[i].tool} → ${thread.queries[i + 1].tool}`;
toolSequences.set(sequence, (toolSequences.get(sequence) || 0) + 1);
}
// Convert to patterns
toolSequences.forEach((count, sequence) => {
if (count >= 2) {
const tools = sequence.split(' → ');
patterns.push({
pattern: `Sequential use of ${sequence}`,
tools_involved: tools,
frequency: count,
confidence: Math.min(0.9, count / thread.queries.length)
});
}
});
// Detect iterative refinement pattern
const sameToolRuns = this.detectConsecutiveTools(thread.queries);
sameToolRuns.forEach(run => {
if (run.count >= 3) {
patterns.push({
pattern: `Iterative refinement with ${run.tool}`,
tools_involved: [run.tool],
frequency: run.count,
confidence: 0.85
});
}
});
return patterns;
}
private generateCrossToolRecommendations(patterns: any[], thread: ConversationThread): string[] {
const recommendations = [];
patterns.forEach(pattern => {
if (pattern.pattern.includes('Sequential use')) {
recommendations.push(
`Consider creating a workflow template for ${pattern.tools_involved.join(' → ')}`
);
}
if (pattern.pattern.includes('Iterative refinement')) {
recommendations.push(
`Multiple ${pattern.tools_involved[0]} runs detected - consider batch processing`
);
}
});
// Add recommendations based on missing tool combinations
const usedTools = new Set(thread.queries.map(q => q.tool));
if (usedTools.has('predict_roi') && !usedTools.has('compare_projects')) {
recommendations.push('Consider comparing this project with alternatives');
}
if (usedTools.size === 1 && thread.queries.length > 3) {
recommendations.push('Leverage other tools for comprehensive analysis');
}
return recommendations;
}
private identifyOptimizations(thread: ConversationThread): string[] {
const optimizations = [];
// Check for redundant queries
const similarQueries = this.findSimilarQueries(thread.queries);
if (similarQueries.length > 0) {
optimizations.push('Detected similar queries - consider caching or parameter optimization');
}
// Check for slow queries
const slowQueries = thread.queries.filter(q =>
q.metadata?.execution_time_ms && q.metadata.execution_time_ms > 5000
);
if (slowQueries.length > 0) {
optimizations.push('Some queries are slow - consider optimizing input parameters');
}
// Check for low confidence results
const lowConfidence = thread.queries.filter(q =>
q.metadata?.confidence_score && q.metadata.confidence_score < 0.7
);
if (lowConfidence.length > 0) {
optimizations.push('Low confidence results detected - enable benchmarks for better accuracy');
}
return optimizations;
}
private getEmptySummary() {
return {
summary: 'No active thread',
key_findings: [],
decisions_made: [],
next_steps: []
};
}
private generateThreadSummary(thread: ConversationThread): string {
const toolUsage = this.getToolUsageStats(thread);
const duration = this.getThreadDuration(thread);
const projectCount = thread.context.project_ids?.length || 0;
return `Analysis session with ${thread.queries.length} queries over ${duration}. ` +
`Tools used: ${toolUsage}. ` +
`${projectCount > 0 ? `Analyzed ${projectCount} project(s).` : ''} ` +
`${thread.context.domain ? `Focus area: ${thread.context.domain}.` : ''}`;
}
private extractKeyFindings(thread: ConversationThread): string[] {
const findings = new Set<string>();
// Extract from query insights
thread.queries.forEach(query => {
query.insights?.forEach(insight => findings.add(insight));
});
// Extract from high-value metrics
if (thread.context.key_metrics?.last_roi && thread.context.key_metrics.last_roi > 150) {
findings.add(`High ROI opportunity: ${thread.context.key_metrics.last_roi}%`);
}
return Array.from(findings).slice(0, 5);
}
private extractDecisions(thread: ConversationThread): string[] {
const decisions: string[] = [];
// Look for recommendation acceptances
thread.queries.forEach(query => {
if (query.output.recommendations?.next_action) {
decisions.push(query.output.recommendations.next_action);
}
});
return Array.from(new Set(decisions));
}
private suggestNextSteps(thread: ConversationThread): string[] {
const steps = [];
const lastQuery = thread.queries[thread.queries.length - 1];
if (lastQuery) {
// Suggest based on last tool used
switch (lastQuery.tool) {
case 'predict_roi':
steps.push('Compare with alternative projects');
steps.push('Create implementation roadmap');
break;
case 'compare_projects':
steps.push('Deep dive into winning project');
steps.push('Analyze risk mitigation strategies');
break;
}
}
// Suggest based on patterns
if (thread.queries.length > 10) {
steps.push('Export analysis results for presentation');
}
return steps;
}
private updateAverage(current: number | undefined, newValue: number): number {
if (!current) return newValue;
return (current + newValue) / 2; // Simplified - in reality would track count
}
private detectConsecutiveTools(queries: QueryContext[]): Array<{tool: string; count: number}> {
const runs: Array<{tool: string; count: number}> = [];
let currentTool: string | null = null;
let count = 0;
queries.forEach(query => {
if (query.tool === currentTool) {
count++;
} else {
if (currentTool && count > 1) {
runs.push({ tool: currentTool, count });
}
currentTool = query.tool;
count = 1;
}
});
if (currentTool && count > 1) {
runs.push({ tool: currentTool, count });
}
return runs;
}
private findSimilarQueries(queries: QueryContext[]): Array<[string, string]> {
const similar: Array<[string, string]> = [];
for (let i = 0; i < queries.length - 1; i++) {
for (let j = i + 1; j < queries.length; j++) {
if (queries[i].tool === queries[j].tool) {
const similarity = this.calculateInputSimilarity(
queries[i].input,
queries[j].input
);
if (similarity > 0.8) {
similar.push([queries[i].id, queries[j].id]);
}
}
}
}
return similar;
}
private calculateInputSimilarity(input1: any, input2: any): number {
// Simplified similarity - in reality would use more sophisticated comparison
const keys1 = Object.keys(input1).sort();
const keys2 = Object.keys(input2).sort();
if (keys1.join(',') !== keys2.join(',')) return 0;
let matchCount = 0;
keys1.forEach(key => {
if (JSON.stringify(input1[key]) === JSON.stringify(input2[key])) {
matchCount++;
}
});
return matchCount / keys1.length;
}
private getToolUsageStats(thread: ConversationThread): string {
const toolCounts = new Map<string, number>();
thread.queries.forEach(query => {
toolCounts.set(query.tool, (toolCounts.get(query.tool) || 0) + 1);
});
return Array.from(toolCounts.entries())
.map(([tool, count]) => `${tool} (${count}x)`)
.join(', ');
}
private getThreadDuration(thread: ConversationThread): string {
const start = new Date(thread.created_at).getTime();
const end = new Date(thread.last_active).getTime();
const durationMs = end - start;
if (durationMs < 60000) {
return `${Math.round(durationMs / 1000)} seconds`;
} else if (durationMs < 3600000) {
return `${Math.round(durationMs / 60000)} minutes`;
} else {
return `${Math.round(durationMs / 3600000)} hours`;
}
}
}
// Export singleton instance
export const crossToolMemory = new CrossToolMemory();