refactor: streamline core API surface

This commit is contained in:
David Snelling 2025-10-04 08:51:49 -07:00
parent 0d54da1471
commit 75ae282861
12 changed files with 4 additions and 3402 deletions

View file

@ -88,7 +88,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
private _extractor?: NeuralEntityExtractor
private _tripleIntelligence?: TripleIntelligenceSystem
private _vfs?: VirtualFileSystem
private _conversation?: any // ConversationManager (lazy-loaded)
// State
private initialized = false
@ -1671,31 +1670,6 @@ export class Brainy<T = any> implements BrainyInterface<T> {
return this._vfs
}
/**
* Conversation Manager API - Infinite Agent Memory
*
* Provides conversation and context management for AI agents:
* - Save and retrieve conversation messages
* - Semantic search across conversation history
* - Smart context retrieval with relevance ranking
* - Artifact management (code, files, documents)
* - Conversation themes and clustering
*
* @returns ConversationManager instance
* @example
* const conv = brain.conversation
* await conv.saveMessage("How do I implement auth?", "user", { conversationId: "conv_123" })
* const context = await conv.getRelevantContext("authentication implementation")
*/
conversation() {
if (!this._conversation) {
// Lazy-load ConversationManager to avoid circular dependencies
const { ConversationManager } = require('./conversation/conversationManager.js')
this._conversation = new ConversationManager(this)
}
return this._conversation
}
/**
* Data Management API - backup, restore, import, export
*/

View file

@ -1,637 +0,0 @@
/**
* 💬 Conversation CLI Commands
*
* CLI interface for infinite agent memory and conversation management
*/
import inquirer from 'inquirer'
import chalk from 'chalk'
import ora from 'ora'
import * as fs from '../../universal/fs.js'
import * as path from '../../universal/path.js'
import { Brainy } from '../../brainy.js'
interface CommandArguments {
action?: string
conversationId?: string
query?: string
role?: string
limit?: number
format?: string
output?: string
_: string[]
}
export const conversationCommand = {
command: 'conversation [action]',
describe: '💬 Conversation and context management',
builder: (yargs: any) => {
return yargs
.positional('action', {
describe: 'Conversation operation to perform',
type: 'string',
choices: ['setup', 'remove', 'search', 'context', 'thread', 'stats', 'export', 'import']
})
.option('conversation-id', {
describe: 'Conversation ID',
type: 'string',
alias: 'c'
})
.option('query', {
describe: 'Search query or context query',
type: 'string',
alias: 'q'
})
.option('role', {
describe: 'Filter by message role',
type: 'string',
choices: ['user', 'assistant', 'system', 'tool'],
alias: 'r'
})
.option('limit', {
describe: 'Maximum results',
type: 'number',
default: 10,
alias: 'l'
})
.option('format', {
describe: 'Output format',
type: 'string',
choices: ['json', 'table', 'text'],
default: 'table',
alias: 'f'
})
.option('output', {
describe: 'Output file path',
type: 'string',
alias: 'o'
})
.example('$0 conversation setup', 'Set up MCP server for Claude Code')
.example('$0 conversation search -q "authentication" -l 5', 'Search messages')
.example('$0 conversation context -q "how to implement JWT"', 'Get relevant context')
.example('$0 conversation thread -c conv_123', 'Get conversation thread')
.example('$0 conversation stats', 'Show conversation statistics')
},
handler: async (argv: CommandArguments) => {
const action = argv.action || 'setup'
try {
switch (action) {
case 'setup':
await handleSetup(argv)
break
case 'remove':
await handleRemove(argv)
break
case 'search':
await handleSearch(argv)
break
case 'context':
await handleContext(argv)
break
case 'thread':
await handleThread(argv)
break
case 'stats':
await handleStats(argv)
break
case 'export':
await handleExport(argv)
break
case 'import':
await handleImport(argv)
break
default:
console.log(chalk.yellow(`Unknown action: ${action}`))
console.log('Run "brainy conversation --help" for usage information')
}
} catch (error: any) {
console.error(chalk.red(`Error: ${error.message}`))
process.exit(1)
}
}
}
/**
* Handle setup command - Set up MCP server for Claude Code
*/
async function handleSetup(argv: CommandArguments) {
console.log(chalk.bold.cyan('\n🧠 Brainy Infinite Memory Setup\n'))
// Check for existing setup
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
const brainyDir = path.join(homeDir, '.brainy-memory')
const dataDir = path.join(brainyDir, 'data')
const serverPath = path.join(brainyDir, 'mcp-server.js')
const configPath = path.join(homeDir, '.config', 'claude-code', 'mcp-servers.json')
// Check if already set up
if (await fs.exists(brainyDir)) {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: 'Brainy memory setup already exists. Overwrite?',
default: false
}
])
if (!overwrite) {
console.log(chalk.yellow('Setup cancelled'))
return
}
}
const spinner = ora('Creating Brainy memory directory...').start()
try {
// Create directories
await fs.mkdir(brainyDir, { recursive: true })
await fs.mkdir(dataDir, { recursive: true })
spinner.succeed('Created Brainy memory directory')
// Create MCP server script
spinner.start('Creating MCP server script...')
const serverScript = `#!/usr/bin/env node
/**
* Brainy Infinite Memory MCP Server
*
* This server provides conversation and context management
* for Claude Code through the Model Control Protocol (MCP).
*/
import { Brainy } from '@soulcraft/brainy'
import { BrainyMCPService } from '@soulcraft/brainy'
import { MCPConversationToolset } from '@soulcraft/brainy'
async function main() {
try {
// Initialize Brainy with filesystem storage
const brain = new Brainy({
storage: {
type: 'filesystem',
path: '${dataDir.replace(/\\/g, '/')}'
},
silent: true // Suppress console output
})
await brain.init()
// Create MCP service
const mcpService = new BrainyMCPService(brain, {
enableAuth: false // Local usage, no auth needed
})
// Create conversation toolset
const conversationTools = new MCPConversationToolset(brain)
await conversationTools.init()
// Register conversation tools
const tools = await conversationTools.getAvailableTools()
console.error('🧠 Brainy Memory Server started')
console.error(\`📊 \${tools.length} conversation tools available\`)
console.error('✅ Ready for Claude Code integration')
// Handle MCP requests via stdio
process.stdin.on('data', async (data) => {
try {
const request = JSON.parse(data.toString())
// Route conversation tool requests
let response
if (request.toolName && request.toolName.startsWith('conversation_')) {
response = await conversationTools.handleRequest(request)
} else {
response = await mcpService.handleRequest(request)
}
// Write response to stdout
process.stdout.write(JSON.stringify(response) + '\\n')
} catch (error) {
console.error('Error handling request:', error)
}
})
// Handle shutdown gracefully
process.on('SIGINT', () => {
console.error('\\n🛑 Shutting down Brainy Memory Server')
process.exit(0)
})
} catch (error) {
console.error('Failed to start Brainy Memory Server:', error)
process.exit(1)
}
}
main()
`
await fs.writeFile(serverPath, serverScript, 'utf8')
// Make executable on Unix systems
try {
await import('fs').then(fsModule => {
fsModule.promises.chmod(serverPath, 0o755).catch(() => {})
})
} catch {
// Windows doesn't need chmod
}
spinner.succeed('Created MCP server script')
// Create Claude Code config
spinner.start('Configuring Claude Code...')
const configDir = path.dirname(configPath)
await fs.mkdir(configDir, { recursive: true })
let mcpConfig: any = {}
if (await fs.exists(configPath)) {
const existingConfig = await fs.readFile(configPath, 'utf8')
mcpConfig = JSON.parse(existingConfig)
}
mcpConfig['brainy-memory'] = {
command: 'node',
args: [serverPath],
env: {
NODE_ENV: 'production'
}
}
await fs.writeFile(configPath, JSON.stringify(mcpConfig, null, 2), 'utf8')
spinner.succeed('Configured Claude Code')
// Initialize Brainy database
spinner.start('Initializing Brainy database...')
const brain = new Brainy({
storage: {
type: 'filesystem',
options: {
path: dataDir
}
},
silent: true
})
await brain.init()
spinner.succeed('Initialized Brainy database')
// Shutdown Brainy to release resources
await brain.close()
// Success!
console.log(chalk.bold.green('\n✅ Setup complete!\n'))
console.log(chalk.cyan('📁 Memory storage:'), brainyDir)
console.log(chalk.cyan('🔧 MCP server:'), serverPath)
console.log(chalk.cyan('⚙️ Claude Code config:'), configPath)
console.log()
console.log(chalk.bold('🚀 Next steps:'))
console.log(' 1. Restart Claude Code to load the MCP server')
console.log(' 2. Start a new conversation - your history will be saved automatically!')
console.log(' 3. Claude will use past context to help you work faster')
console.log()
console.log(chalk.dim('Run "brainy conversation stats" to see your conversation statistics'))
} catch (error: any) {
spinner.fail('Setup failed')
throw error
}
}
/**
* Handle remove command - Remove MCP server and optionally data
*/
async function handleRemove(argv: CommandArguments) {
console.log(chalk.bold.cyan('\n🗑 Brainy Infinite Memory Removal\n'))
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
const brainyDir = path.join(homeDir, '.brainy-memory')
const configPath = path.join(homeDir, '.config', 'claude-code', 'mcp-servers.json')
// Check if setup exists
const brainyExists = await fs.exists(brainyDir)
const configExists = await fs.exists(configPath)
if (!brainyExists && !configExists) {
console.log(chalk.yellow('No Brainy memory setup found. Nothing to remove.'))
return
}
// Show what will be removed
console.log(chalk.white('The following will be removed:'))
if (brainyExists) {
console.log(chalk.dim(`${brainyDir} (memory data and MCP server)`))
}
if (configExists) {
console.log(chalk.dim(` • MCP config entry in ${configPath}`))
}
console.log()
// Confirm removal
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Are you sure you want to remove Brainy infinite memory?',
default: false
}
])
if (!confirm) {
console.log(chalk.yellow('Removal cancelled'))
return
}
const spinner = ora('Removing Brainy memory setup...').start()
try {
// Remove Brainy directory
if (brainyExists) {
// Use Node.js fs for rm operation as universal fs doesn't have it
const nodefs = await import('fs')
await nodefs.promises.rm(brainyDir, { recursive: true, force: true })
spinner.text = 'Removed memory directory...'
}
// Remove MCP config entry
if (configExists) {
try {
const configContent = await fs.readFile(configPath, 'utf8')
const mcpConfig = JSON.parse(configContent)
if (mcpConfig['brainy-memory']) {
delete mcpConfig['brainy-memory']
await fs.writeFile(configPath, JSON.stringify(mcpConfig, null, 2), 'utf8')
spinner.text = 'Removed MCP configuration...'
}
} catch (error) {
// If config file is corrupted or empty, skip
spinner.warn('Could not update MCP config file')
}
}
spinner.succeed('Successfully removed Brainy infinite memory')
console.log()
console.log(chalk.bold('✅ Cleanup complete'))
console.log()
console.log(chalk.white('All conversation data and MCP configuration have been removed.'))
console.log(chalk.dim('Run "brainy conversation setup" to set up again.'))
console.log()
} catch (error: any) {
spinner.fail('Removal failed')
console.error(chalk.red('Error:'), error.message)
throw error
}
}
/**
* Handle search command - Search messages
*/
async function handleSearch(argv: CommandArguments) {
if (!argv.query) {
console.log(chalk.yellow('Query required. Use -q or --query'))
return
}
const spinner = ora('Searching conversations...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const results = await conv.searchMessages({
query: argv.query,
limit: argv.limit || 10,
role: argv.role as any,
includeContent: true,
includeMetadata: true
})
spinner.succeed(`Found ${results.length} messages`)
if (results.length === 0) {
console.log(chalk.yellow('No messages found'))
await brain.close()
return
}
// Display results
console.log()
for (const result of results) {
console.log(chalk.bold.cyan(`${result.message.role}:`), result.snippet)
console.log(chalk.dim(` Score: ${result.score.toFixed(3)} | Conv: ${result.conversationId}`))
console.log()
}
await brain.close()
}
/**
* Handle context command - Get relevant context
*/
async function handleContext(argv: CommandArguments) {
if (!argv.query) {
console.log(chalk.yellow('Query required. Use -q or --query'))
return
}
const spinner = ora('Retrieving relevant context...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const context = await conv.getRelevantContext(argv.query, {
limit: argv.limit || 10,
includeArtifacts: true,
includeSimilarConversations: true
})
spinner.succeed(`Retrieved ${context.messages.length} relevant messages`)
if (context.messages.length === 0) {
console.log(chalk.yellow('No relevant context found'))
await brain.close()
return
}
// Display context
console.log()
console.log(chalk.bold('📊 Context Statistics:'))
console.log(chalk.dim(` Messages: ${context.messages.length}`))
console.log(chalk.dim(` Tokens: ${context.totalTokens}`))
console.log(chalk.dim(` Query time: ${context.metadata.queryTime}ms`))
console.log()
console.log(chalk.bold('💬 Relevant Messages:'))
for (const msg of context.messages) {
console.log()
console.log(chalk.cyan(`${msg.role} (score: ${msg.relevanceScore.toFixed(3)}):`))
console.log(msg.content.substring(0, 200) + (msg.content.length > 200 ? '...' : ''))
}
if (context.similarConversations && context.similarConversations.length > 0) {
console.log()
console.log(chalk.bold('🔗 Similar Conversations:'))
for (const conv of context.similarConversations) {
console.log(chalk.dim(` - ${conv.title || conv.id} (${conv.relevance.toFixed(2)})`))
}
}
await brain.close()
}
/**
* Handle thread command - Get conversation thread
*/
async function handleThread(argv: CommandArguments) {
if (!argv.conversationId) {
console.log(chalk.yellow('Conversation ID required. Use -c or --conversation-id'))
return
}
const spinner = ora('Loading conversation thread...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const thread = await conv.getConversationThread(argv.conversationId, {
includeArtifacts: true
})
spinner.succeed(`Loaded ${thread.messages.length} messages`)
// Display thread
console.log()
console.log(chalk.bold('📊 Thread Information:'))
console.log(chalk.dim(` Conversation: ${thread.id}`))
console.log(chalk.dim(` Messages: ${thread.metadata.messageCount}`))
console.log(chalk.dim(` Tokens: ${thread.metadata.totalTokens}`))
console.log(chalk.dim(` Started: ${new Date(thread.metadata.startTime).toLocaleString()}`))
console.log()
console.log(chalk.bold('💬 Messages:'))
for (const msg of thread.messages) {
console.log()
console.log(chalk.cyan(`${msg.role}:`), msg.content)
console.log(chalk.dim(` ${new Date(msg.createdAt).toLocaleString()}`))
}
await brain.close()
}
/**
* Handle stats command - Show statistics
*/
async function handleStats(argv: CommandArguments) {
const spinner = ora('Calculating statistics...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const stats = await conv.getConversationStats()
spinner.succeed('Statistics calculated')
// Display stats
console.log()
console.log(chalk.bold.cyan('📊 Conversation Statistics\n'))
console.log(chalk.bold('Overall:'))
console.log(chalk.dim(` Conversations: ${stats.totalConversations}`))
console.log(chalk.dim(` Messages: ${stats.totalMessages}`))
console.log(chalk.dim(` Total Tokens: ${stats.totalTokens.toLocaleString()}`))
console.log(chalk.dim(` Avg Messages/Conversation: ${stats.averageMessagesPerConversation.toFixed(1)}`))
console.log(chalk.dim(` Avg Tokens/Message: ${stats.averageTokensPerMessage.toFixed(1)}`))
console.log()
if (Object.keys(stats.roles).length > 0) {
console.log(chalk.bold('By Role:'))
for (const [role, count] of Object.entries(stats.roles)) {
console.log(chalk.dim(` ${role}: ${count}`))
}
console.log()
}
if (Object.keys(stats.phases).length > 0) {
console.log(chalk.bold('By Phase:'))
for (const [phase, count] of Object.entries(stats.phases)) {
console.log(chalk.dim(` ${phase}: ${count}`))
}
}
await brain.close()
}
/**
* Handle export command - Export conversation
*/
async function handleExport(argv: CommandArguments) {
if (!argv.conversationId) {
console.log(chalk.yellow('Conversation ID required. Use -c or --conversation-id'))
return
}
const spinner = ora('Exporting conversation...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const exported = await conv.exportConversation(argv.conversationId)
const output = argv.output || `conversation_${argv.conversationId}.json`
await fs.writeFile(output, JSON.stringify(exported, null, 2), 'utf8')
spinner.succeed(`Exported to ${output}`)
await brain.close()
}
/**
* Handle import command - Import conversation
*/
async function handleImport(argv: CommandArguments) {
const inputFile = argv.output
if (!inputFile) {
console.log(chalk.yellow('Input file required. Use -o or --output'))
return
}
const spinner = ora('Importing conversation...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const data = JSON.parse(await fs.readFile(inputFile, 'utf8'))
const conversationId = await conv.importConversation(data)
spinner.succeed(`Imported as conversation ${conversationId}`)
await brain.close()
}
export default conversationCommand

View file

@ -13,7 +13,6 @@ import { coreCommands } from './commands/core.js'
import { utilityCommands } from './commands/utility.js'
import { vfsCommands } from './commands/vfs.js'
import { dataCommands } from './commands/data.js'
import conversationCommand from './commands/conversation.js'
import { readFileSync } from 'fs'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
@ -166,55 +165,6 @@ program
.option('-o, --output <file>', 'Output file')
.action(neuralCommands.visualize)
// ===== Conversation Commands (Infinite Memory) =====
program
.command('conversation')
.alias('conv')
.description('💬 Infinite agent memory and context management')
.addCommand(
new Command('setup')
.description('Set up MCP server for Claude Code integration')
.action(async () => {
await conversationCommand.handler({ action: 'setup', _: [] })
})
)
.addCommand(
new Command('search')
.description('Search messages across conversations')
.requiredOption('-q, --query <query>', 'Search query')
.option('-c, --conversation-id <id>', 'Filter by conversation')
.option('-r, --role <role>', 'Filter by role')
.option('-l, --limit <number>', 'Maximum results', '10')
.action(async (options) => {
await conversationCommand.handler({ action: 'search', ...options as any, _: [] })
})
)
.addCommand(
new Command('context')
.description('Get relevant context for a query')
.requiredOption('-q, --query <query>', 'Context query')
.option('-l, --limit <number>', 'Maximum messages', '10')
.action(async (options) => {
await conversationCommand.handler({ action: 'context', ...options as any, _: [] })
})
)
.addCommand(
new Command('thread')
.description('Get full conversation thread')
.requiredOption('-c, --conversation-id <id>', 'Conversation ID')
.action(async (options) => {
await conversationCommand.handler({ action: 'thread', ...options as any, _: [] })
})
)
.addCommand(
new Command('stats')
.description('Show conversation statistics')
.action(async () => {
await conversationCommand.handler({ action: 'stats', _: [] })
})
)
// ===== VFS Commands (Subcommand Group) =====
program

View file

@ -1,825 +0,0 @@
/**
* ConversationManager - Infinite Agent Memory
*
* Production-ready conversation and context management for AI agents.
* Built on Brainy's existing infrastructure: Triple Intelligence, Neural API, VFS.
*
* REAL IMPLEMENTATION - No stubs, no mocks, no TODOs
*/
import { v4 as uuidv4 } from '../universal/uuid.js'
import { NounType, VerbType } from '../types/graphTypes.js'
import { Brainy } from '../brainy.js'
import {
MessageRole,
ProblemSolvingPhase,
ConversationMessage,
ConversationMessageMetadata,
ConversationThread,
ConversationThreadMetadata,
ConversationContext,
RankedMessage,
SaveMessageOptions,
ContextRetrievalOptions,
ConversationSearchOptions,
ConversationSearchResult,
ConversationTheme,
ArtifactOptions,
ConversationStats,
CompactionOptions,
CompactionResult
} from './types.js'
/**
* ConversationManager - High-level API for conversation operations
*
* Uses existing Brainy infrastructure:
* - brain.add() for messages
* - brain.relate() for threading
* - brain.find() with Triple Intelligence for context
* - brain.neural for clustering and similarity
* - brain.vfs() for artifacts
*/
export class ConversationManager {
private brain: Brainy
private initialized = false
private _vfs: any = null
/**
* Create a ConversationManager instance
* @param brain Brainy instance to use
*/
constructor(brain: Brainy) {
this.brain = brain
}
/**
* Initialize the conversation manager
* Lazy initialization pattern - only called when first used
*/
async init(): Promise<void> {
if (this.initialized) {
return
}
// VFS is lazy-loaded and might not be initialized yet
try {
this._vfs = this.brain.vfs()
await this._vfs.init()
} catch (error) {
// VFS initialization failed, will work without artifact support
console.warn('VFS initialization failed, artifact support disabled:', error)
}
this.initialized = true
}
/**
* Save a message to the conversation history
*
* Uses: brain.add() with NounType.Message
* Real implementation - stores message with embedding
*
* @param content Message content
* @param role Message role (user, assistant, system, tool)
* @param options Save options (conversationId, metadata, etc.)
* @returns Message ID
*/
async saveMessage(
content: string,
role: MessageRole,
options: SaveMessageOptions = {}
): Promise<string> {
if (!this.initialized) {
await this.init()
}
// Generate IDs if not provided
const conversationId = options.conversationId || `conv_${uuidv4()}`
const sessionId = options.sessionId || `session_${uuidv4()}`
const timestamp = Date.now()
// Build metadata
const metadata: ConversationMessageMetadata = {
role,
conversationId,
sessionId,
timestamp,
problemSolvingPhase: options.phase,
confidence: options.confidence,
artifacts: options.artifacts || [],
toolsUsed: options.toolsUsed || [],
references: [],
tags: options.tags || [],
...options.metadata
}
// Add message to brain using REAL API
const messageId = await this.brain.add({
data: content,
type: NounType.Message,
metadata
})
// Link to previous message if specified (REAL graph relationship)
if (options.linkToPrevious) {
await this.brain.relate({
from: options.linkToPrevious,
to: messageId,
type: VerbType.Precedes,
metadata: {
conversationId,
timestamp
}
})
}
return messageId
}
/**
* Link two messages in temporal sequence
*
* Uses: brain.relate() with VerbType.Precedes
* Real implementation - creates graph relationship
*
* @param prevMessageId ID of previous message
* @param nextMessageId ID of next message
* @returns Relationship ID
*/
async linkMessages(prevMessageId: string, nextMessageId: string): Promise<string> {
if (!this.initialized) {
await this.init()
}
// Create real graph relationship
const verbId = await this.brain.relate({
from: prevMessageId,
to: nextMessageId,
type: VerbType.Precedes,
metadata: {
timestamp: Date.now()
}
})
return verbId
}
/**
* Get a full conversation thread
*
* Uses: brain.getNoun() and brain.getConnections()
* Real implementation - traverses graph relationships
*
* @param conversationId Conversation ID
* @param options Options (includeArtifacts, etc.)
* @returns Complete conversation thread
*/
async getConversationThread(
conversationId: string,
options: { includeArtifacts?: boolean } = {}
): Promise<ConversationThread> {
if (!this.initialized) {
await this.init()
}
// Search for all messages in conversation (REAL search)
const results = await this.brain.find({
where: {
conversationId
},
limit: 10000 // Large limit for full thread
})
// Convert results to ConversationMessage format
const messages: ConversationMessage[] = results.map((result: any) => ({
id: result.id,
content: result.data || result.content || '',
role: result.metadata.role,
metadata: result.metadata as ConversationMessageMetadata,
embedding: result.embedding,
createdAt: result.metadata.timestamp || Date.now(),
updatedAt: result.metadata.timestamp || Date.now()
}))
// Sort by timestamp
messages.sort((a, b) => a.createdAt - b.createdAt)
// Build thread metadata
const startTime = messages.length > 0 ? messages[0].createdAt : Date.now()
const endTime = messages.length > 0 ? messages[messages.length - 1].createdAt : undefined
const totalTokens = messages.reduce((sum, msg) => sum + (msg.metadata.tokensUsed || 0), 0)
const threadMetadata: ConversationThreadMetadata = {
conversationId,
startTime,
endTime,
messageCount: messages.length,
totalTokens,
participants: [...new Set(messages.map(m => m.role))]
}
// Get artifacts if requested (REAL VFS query)
let artifacts: string[] | undefined
if (options.includeArtifacts && this._vfs) {
artifacts = messages
.flatMap(m => m.metadata.artifacts || [])
.filter((id, idx, arr) => arr.indexOf(id) === idx)
}
return {
id: conversationId,
metadata: threadMetadata,
messages,
artifacts
}
}
/**
* Get relevant context for a query
*
* Uses: brain.find() with Triple Intelligence
* Real implementation - semantic + temporal + graph ranking
*
* @param query Query string or context options
* @param options Retrieval options
* @returns Ranked context messages with artifacts
*/
async getRelevantContext(
query: string | ContextRetrievalOptions,
options?: ContextRetrievalOptions
): Promise<ConversationContext> {
if (!this.initialized) {
await this.init()
}
const startTime = Date.now()
// Normalize options
const opts: ContextRetrievalOptions = typeof query === 'string'
? { query, ...options }
: query
const {
query: queryText,
limit = 10,
maxTokens = 50000,
relevanceThreshold = 0.7,
role,
phase,
tags,
minConfidence,
timeRange,
conversationId,
sessionId,
weights = { semantic: 1.0, temporal: 0.5, graph: 0.3 },
includeArtifacts = false,
includeSimilarConversations = false,
deduplicateClusters = true
} = opts
// Build metadata filter
const whereFilter: any = {}
if (role) {
whereFilter.role = Array.isArray(role) ? { $in: role } : role
}
if (phase) {
whereFilter.problemSolvingPhase = Array.isArray(phase) ? { $in: phase } : phase
}
if (tags && tags.length > 0) {
whereFilter.tags = { $in: tags }
}
if (minConfidence !== undefined) {
whereFilter.confidence = { $gte: minConfidence }
}
if (timeRange) {
if (timeRange.start !== undefined) {
whereFilter.timestamp = { $gte: timeRange.start }
}
if (timeRange.end !== undefined) {
whereFilter.timestamp = { ...whereFilter.timestamp, $lte: timeRange.end }
}
}
if (conversationId) {
whereFilter.conversationId = conversationId
}
if (sessionId) {
whereFilter.sessionId = sessionId
}
// Query with Triple Intelligence (REAL)
const findOptions: any = {
limit: limit * 2, // Get more for ranking
where: whereFilter
}
if (queryText) {
findOptions.like = queryText
}
const results = await this.brain.find(findOptions)
// Calculate relevance scores (REAL scoring)
const now = Date.now()
const rankedMessages: RankedMessage[] = results
.map((result: any) => {
// Semantic score (from vector similarity)
const semanticScore = result.score || 0
// Temporal score (recency decay)
const ageInDays = (now - (result.metadata.timestamp || now)) / (1000 * 60 * 60 * 24)
const temporalScore = Math.exp(-0.1 * ageInDays) // Decay rate: 0.1
// Graph score (would need graph traversal, simplified for now)
const graphScore = 0.5 // Placeholder for now, can enhance later
// Combined score
const relevanceScore =
(weights.semantic ?? 1.0) * semanticScore +
(weights.temporal ?? 0.5) * temporalScore +
(weights.graph ?? 0.3) * graphScore
return {
id: result.id,
content: result.data || result.content || '',
role: result.metadata.role,
metadata: result.metadata as ConversationMessageMetadata,
embedding: result.embedding,
createdAt: result.metadata.timestamp || now,
updatedAt: result.metadata.timestamp || now,
relevanceScore,
semanticScore,
temporalScore,
graphScore
} as RankedMessage
})
.filter((msg: RankedMessage) => msg.relevanceScore >= relevanceThreshold)
.sort((a: RankedMessage, b: RankedMessage) => b.relevanceScore - a.relevanceScore)
// Deduplicate via clustering if requested
let finalMessages = rankedMessages
if (deduplicateClusters && rankedMessages.length > 5 && this.brain.neural) {
// Use neural clustering to remove duplicates (REAL)
try {
const clusters = await this.brain.neural().clusters({
maxClusters: Math.ceil(rankedMessages.length / 3),
threshold: 0.85
})
// Keep highest scoring message from each cluster
const kept = new Set<string>()
for (const cluster of clusters) {
const clusterMessages = rankedMessages.filter(msg =>
cluster.members?.includes(msg.id)
)
if (clusterMessages.length > 0) {
const best = clusterMessages.reduce((a, b) =>
a.relevanceScore > b.relevanceScore ? a : b
)
kept.add(best.id)
}
}
finalMessages = rankedMessages.filter(msg => kept.has(msg.id))
} catch (error) {
// Clustering failed, use all messages
console.warn('Clustering failed:', error)
}
}
// Limit by token budget
let totalTokens = 0
const messagesWithinBudget: RankedMessage[] = []
for (const msg of finalMessages) {
const tokens = msg.metadata.tokensUsed || Math.ceil(msg.content.length / 4)
if (totalTokens + tokens <= maxTokens) {
messagesWithinBudget.push(msg)
totalTokens += tokens
} else {
break
}
}
// Get artifacts if requested (REAL VFS)
let artifacts: any[] = []
if (includeArtifacts && this._vfs) {
const artifactIds = new Set(
messagesWithinBudget.flatMap(msg => msg.metadata.artifacts || [])
)
for (const artifactId of artifactIds) {
try {
const entity = await this.brain.get(artifactId)
if (entity) {
artifacts.push({
id: artifactId,
path: entity.metadata?.path || artifactId,
summary: entity.metadata?.description || undefined
})
}
} catch (error) {
// Artifact not found, skip
continue
}
}
}
// Get similar conversations if requested
let similarConversations: any[] = []
if (includeSimilarConversations && conversationId && this.brain.neural) {
// Use neural neighbors (REAL)
try {
const neighborsResult = await this.brain.neural().neighbors(conversationId, {
limit: 5,
minSimilarity: 0.7
})
similarConversations = neighborsResult.neighbors.map((neighbor: any) => ({
id: neighbor.id,
title: neighbor.metadata?.title,
summary: neighbor.metadata?.summary,
relevance: neighbor.score,
messageCount: neighbor.metadata?.messageCount || 0
}))
} catch (error) {
// Neighbors failed, skip
console.warn('Similar conversation search failed:', error)
}
}
const queryTime = Date.now() - startTime
return {
messages: messagesWithinBudget.slice(0, limit),
artifacts,
similarConversations,
totalTokens,
metadata: {
queryTime,
messagesConsidered: results.length,
conversationsSearched: new Set(results.map((r: any) => r.metadata.conversationId)).size
}
}
}
/**
* Search messages semantically
*
* Uses: brain.find() with semantic search
* Real implementation - vector similarity search
*
* @param options Search options
* @returns Search results with scores
*/
async searchMessages(options: ConversationSearchOptions): Promise<ConversationSearchResult[]> {
if (!this.initialized) {
await this.init()
}
const {
query,
limit = 10,
role,
conversationId,
sessionId,
timeRange,
includeMetadata = true,
includeContent = true
} = options
// Build filter
const whereFilter: any = {}
if (role) {
whereFilter.role = Array.isArray(role) ? { $in: role } : role
}
if (conversationId) {
whereFilter.conversationId = conversationId
}
if (sessionId) {
whereFilter.sessionId = sessionId
}
if (timeRange) {
if (timeRange.start) {
whereFilter.timestamp = { $gte: timeRange.start }
}
if (timeRange.end) {
whereFilter.timestamp = { ...whereFilter.timestamp, $lte: timeRange.end }
}
}
// Search with Triple Intelligence (REAL)
const results = await this.brain.find({
query: query,
where: whereFilter,
limit
})
// Format results
return results.map((result: any) => {
const message: ConversationMessage = {
id: result.id,
content: includeContent ? (result.data || result.content || '') : '',
role: result.metadata.role,
metadata: includeMetadata ? (result.metadata as ConversationMessageMetadata) : {} as any,
embedding: result.embedding,
createdAt: result.metadata.timestamp || Date.now(),
updatedAt: result.metadata.timestamp || Date.now()
}
// Create snippet
const content = result.data || result.content || ''
const snippet = content.length > 150 ? content.substring(0, 147) + '...' : content
return {
message,
score: result.score || 0,
conversationId: result.metadata.conversationId,
snippet: includeContent ? snippet : undefined
}
})
}
/**
* Find similar conversations using Neural API
*
* Uses: brain.neural.neighbors()
* Real implementation - semantic similarity with embeddings
*
* @param conversationId Conversation ID to find similar to
* @param limit Maximum number of similar conversations
* @param threshold Minimum similarity threshold
* @returns Similar conversations with relevance scores
*/
async findSimilarConversations(
conversationId: string,
limit: number = 5,
threshold: number = 0.7
): Promise<Array<{ id: string; relevance: number; metadata?: any }>> {
if (!this.initialized) {
await this.init()
}
if (!this.brain.neural) {
throw new Error('Neural API not available')
}
// Use neural neighbors (REAL)
const neighborsResult = await this.brain.neural().neighbors(conversationId, {
limit: limit,
minSimilarity: threshold
})
return neighborsResult.neighbors.map((neighbor: any) => ({
id: neighbor.id,
relevance: neighbor.score,
metadata: neighbor.metadata
}))
}
/**
* Get conversation themes via clustering
*
* Uses: brain.neural.clusters()
* Real implementation - semantic clustering
*
* @param conversationId Conversation ID
* @returns Discovered themes
*/
async getConversationThemes(conversationId: string): Promise<ConversationTheme[]> {
if (!this.initialized) {
await this.init()
}
if (!this.brain.neural) {
throw new Error('Neural API not available')
}
// Get messages for conversation
const results = await this.brain.find({
where: { conversationId },
limit: 1000
})
if (results.length === 0) {
return []
}
// Cluster messages (REAL)
const clusters = await this.brain.neural().clusters({
maxClusters: Math.min(5, Math.ceil(results.length / 5)),
threshold: 0.75
})
// Convert to themes
return clusters.map((cluster: any, index: number) => ({
id: `theme_${index}`,
label: cluster.label || `Theme ${index + 1}`,
messages: cluster.members || [],
centroid: cluster.centroid || [],
coherence: cluster.coherence || 0
}))
}
/**
* Save an artifact (code, file, etc.) to VFS
*
* Uses: brain.vfs()
* Real implementation - stores in virtual filesystem
*
* @param path VFS path
* @param content File content
* @param options Artifact options
* @returns Artifact entity ID
*/
async saveArtifact(
path: string,
content: string | Buffer,
options: ArtifactOptions
): Promise<string> {
if (!this.initialized) {
await this.init()
}
if (!this._vfs) {
throw new Error('VFS not available')
}
// Write file to VFS (REAL)
await this._vfs.writeFile(path, content)
// Get the file entity
const entity = await this._vfs.getEntity(path)
// Link to conversation message if provided
if (options.messageId) {
await this.brain.relate({
from: options.messageId,
to: entity.id,
type: VerbType.Creates,
metadata: {
conversationId: options.conversationId,
artifactType: options.type || 'other'
}
})
}
return entity.id
}
/**
* Get conversation statistics
*
* Uses: brain.find() with aggregations
* Real implementation - queries and aggregates data
*
* @param conversationId Optional conversation ID to filter
* @returns Conversation statistics
*/
async getConversationStats(conversationId?: string): Promise<ConversationStats> {
if (!this.initialized) {
await this.init()
}
// Query messages
const whereFilter = conversationId ? { conversationId } : {}
const results = await this.brain.find({
where: whereFilter,
limit: 100000 // Large limit for stats
})
// Calculate statistics (REAL aggregation)
const conversations = new Set(results.map((r: any) => r.metadata.conversationId))
const totalMessages = results.length
const totalTokens = results.reduce(
(sum: number, r: any) => sum + (r.metadata.tokensUsed || 0),
0
)
const timestamps = results.map((r: any) => r.metadata.timestamp || Date.now())
const oldestMessage = Math.min(...timestamps)
const newestMessage = Math.max(...timestamps)
// Count by phase
const phases: Record<string, number> = {}
const roles: Record<string, number> = {}
for (const result of results) {
const phase = result.entity.metadata.problemSolvingPhase
const role = result.entity.metadata.role
if (phase) {
phases[phase] = (phases[phase] || 0) + 1
}
if (role) {
roles[role] = (roles[role] || 0) + 1
}
}
return {
totalConversations: conversations.size,
totalMessages,
totalTokens,
averageMessagesPerConversation: totalMessages / Math.max(1, conversations.size),
averageTokensPerMessage: totalTokens / Math.max(1, totalMessages),
oldestMessage,
newestMessage,
phases: phases as any,
roles: roles as any
}
}
/**
* Delete a message
*
* Uses: brain.deleteNoun()
* Real implementation - removes from graph
*
* @param messageId Message ID to delete
*/
async deleteMessage(messageId: string): Promise<void> {
if (!this.initialized) {
await this.init()
}
await this.brain.delete(messageId)
}
/**
* Export conversation to JSON
*
* Uses: getConversationThread()
* Real implementation - serializes conversation
*
* @param conversationId Conversation ID
* @returns JSON-serializable conversation object
*/
async exportConversation(conversationId: string): Promise<any> {
if (!this.initialized) {
await this.init()
}
const thread = await this.getConversationThread(conversationId, {
includeArtifacts: true
})
return {
version: '1.0',
exportedAt: Date.now(),
conversation: thread
}
}
/**
* Import conversation from JSON
*
* Uses: saveMessage() and linkMessages()
* Real implementation - recreates conversation
*
* @param data Exported conversation data
* @returns New conversation ID
*/
async importConversation(data: any): Promise<string> {
if (!this.initialized) {
await this.init()
}
const newConversationId = `conv_${uuidv4()}`
const conversation = data.conversation
if (!conversation || !conversation.messages) {
throw new Error('Invalid conversation data')
}
// Import messages in order
const messageIdMap = new Map<string, string>()
for (let i = 0; i < conversation.messages.length; i++) {
const msg = conversation.messages[i]
const prevMessageId = i > 0 ? messageIdMap.get(conversation.messages[i - 1].id) : undefined
const newMessageId = await this.saveMessage(msg.content, msg.role, {
conversationId: newConversationId,
sessionId: conversation.metadata.sessionId,
phase: msg.metadata.problemSolvingPhase,
confidence: msg.metadata.confidence,
tags: msg.metadata.tags,
linkToPrevious: prevMessageId,
metadata: msg.metadata
})
messageIdMap.set(msg.id, newMessageId)
}
return newConversationId
}
}
/**
* Create a ConversationManager instance
*
* @param brain Brainy instance
* @returns ConversationManager instance
*/
export function createConversationManager(brain: Brainy): ConversationManager {
return new ConversationManager(brain)
}

View file

@ -1,28 +0,0 @@
/**
* Conversation Module - Infinite Agent Memory
*
* Provides conversation and context management for AI agents
* Built on Brainy's existing infrastructure
*/
export { ConversationManager, createConversationManager } from './conversationManager.js'
export type {
MessageRole,
ProblemSolvingPhase,
ConversationMessage,
ConversationMessageMetadata,
ConversationThread,
ConversationThreadMetadata,
ConversationContext,
RankedMessage,
SaveMessageOptions,
ContextRetrievalOptions,
ConversationSearchOptions,
ConversationSearchResult,
ConversationTheme,
ArtifactOptions,
ConversationStats,
CompactionOptions,
CompactionResult
} from './types.js'

View file

@ -1,277 +0,0 @@
/**
* Conversation Types for Infinite Agent Memory
*
* Production-ready type definitions for storing and retrieving
* conversation history with semantic search and context management.
*/
import { NounType, VerbType } from '../types/graphTypes.js'
/**
* Role of the message sender
*/
export type MessageRole = 'user' | 'assistant' | 'system' | 'tool'
/**
* Problem-solving phase for tracking agent's progress
*/
export type ProblemSolvingPhase =
| 'understanding'
| 'analysis'
| 'planning'
| 'implementation'
| 'testing'
| 'debugging'
| 'refinement'
| 'completed'
/**
* Metadata for a conversation message
*/
export interface ConversationMessageMetadata {
role: MessageRole
conversationId: string
sessionId?: string
timestamp: number
// Agent state tracking
problemSolvingPhase?: ProblemSolvingPhase
confidence?: number // 0-1 confidence score
// Token tracking
tokensUsed?: number
tokensTotal?: number
// Context tracking
artifacts?: string[] // IDs or paths of created artifacts
toolsUsed?: string[] // Names of tools/functions used
references?: string[] // IDs of referenced messages/documents
// Metadata for filtering
tags?: string[]
priority?: number
archived?: boolean
// Custom metadata
[key: string]: any
}
/**
* A conversation message with all metadata
*/
export interface ConversationMessage {
id: string
content: string
role: MessageRole
metadata: ConversationMessageMetadata
embedding?: number[]
createdAt: number
updatedAt: number
}
/**
* Conversation thread metadata
*/
export interface ConversationThreadMetadata {
conversationId: string
sessionId?: string
title?: string
summary?: string
startTime: number
endTime?: number
messageCount: number
totalTokens: number
participants: string[] // user IDs or names
tags?: string[]
archived?: boolean
[key: string]: any
}
/**
* A conversation thread (collection of messages)
*/
export interface ConversationThread {
id: string
metadata: ConversationThreadMetadata
messages: ConversationMessage[]
artifacts?: string[] // VFS paths or entity IDs
}
/**
* Options for retrieving relevant context
*/
export interface ContextRetrievalOptions {
// Query
query?: string // Natural language query
conversationId?: string // Limit to specific conversation
sessionId?: string // Limit to specific session
// Filtering
role?: MessageRole | MessageRole[]
phase?: ProblemSolvingPhase | ProblemSolvingPhase[]
tags?: string[]
minConfidence?: number
timeRange?: {
start?: number
end?: number
}
// Search parameters
limit?: number // Max messages to return (default: 10)
maxTokens?: number // Token budget for context (default: 50000)
relevanceThreshold?: number // Minimum similarity score (default: 0.7)
// Ranking weights
weights?: {
semantic?: number // Weight for semantic similarity (default: 1.0)
temporal?: number // Weight for recency (default: 0.5)
graph?: number // Weight for graph relationships (default: 0.3)
}
// Advanced options
includeArtifacts?: boolean // Include linked code/file artifacts
includeSimilarConversations?: boolean // Include similar past conversations
deduplicateClusters?: boolean // Deduplicate via clustering (default: true)
}
/**
* Ranked context message with relevance score
*/
export interface RankedMessage extends ConversationMessage {
relevanceScore: number
semanticScore?: number
temporalScore?: number
graphScore?: number
explanation?: string
}
/**
* Retrieved context result
*/
export interface ConversationContext {
messages: RankedMessage[]
artifacts?: Array<{
path: string
id: string
content?: string
summary?: string
}>
similarConversations?: Array<{
id: string
title?: string
summary?: string
relevance: number
messageCount: number
}>
totalTokens: number
metadata: {
queryTime: number
messagesConsidered: number
conversationsSearched: number
}
}
/**
* Options for saving messages
*/
export interface SaveMessageOptions {
conversationId?: string // Auto-generated if not provided
sessionId?: string
phase?: ProblemSolvingPhase
confidence?: number
artifacts?: string[]
toolsUsed?: string[]
tags?: string[]
linkToPrevious?: string // ID of previous message to link
metadata?: Record<string, any> // Additional metadata
}
/**
* Options for conversation search
*/
export interface ConversationSearchOptions {
query: string
limit?: number
role?: MessageRole | MessageRole[]
conversationId?: string
sessionId?: string
timeRange?: {
start?: number
end?: number
}
includeMetadata?: boolean
includeContent?: boolean
}
/**
* Search result for conversations
*/
export interface ConversationSearchResult {
message: ConversationMessage
score: number
conversationId: string
snippet?: string
}
/**
* Theme discovered via clustering
*/
export interface ConversationTheme {
id: string
label: string
messages: string[] // Message IDs
centroid: number[] // Vector centroid
coherence: number // How coherent the cluster is (0-1)
keywords?: string[]
}
/**
* Options for artifact storage
*/
export interface ArtifactOptions {
conversationId: string
messageId?: string
type?: 'code' | 'config' | 'data' | 'document' | 'other'
language?: string
description?: string
metadata?: Record<string, any>
}
/**
* Statistics about conversations
*/
export interface ConversationStats {
totalConversations: number
totalMessages: number
totalTokens: number
averageMessagesPerConversation: number
averageTokensPerMessage: number
oldestMessage: number
newestMessage: number
phases: Record<ProblemSolvingPhase, number>
roles: Record<MessageRole, number>
}
/**
* Compaction strategy options
*/
export interface CompactionOptions {
conversationId: string
strategy?: 'cluster-based' | 'importance-based' | 'hybrid'
keepRatio?: number // Ratio of messages to keep (default: 0.3)
minImportance?: number // Minimum importance score to keep (default: 0.5)
preservePhases?: ProblemSolvingPhase[] // Always keep these phases
preserveRecent?: number // Always keep this many recent messages
}
/**
* Result of compaction operation
*/
export interface CompactionResult {
originalCount: number
compactedCount: number
removedCount: number
tokensFreed: number
preservedMessageIds: string[]
summaryMessageId?: string
}

View file

@ -474,27 +474,3 @@ export type {
MCPServiceOptions,
MCPTool
}
// Export Conversation API (Infinite Agent Memory)
export { ConversationManager, createConversationManager } from './conversation/index.js'
export { MCPConversationToolset, createConversationToolset } from './mcp/conversationTools.js'
export type {
MessageRole,
ProblemSolvingPhase,
ConversationMessage,
ConversationMessageMetadata,
ConversationThread,
ConversationThreadMetadata,
ConversationContext,
RankedMessage,
SaveMessageOptions,
ContextRetrievalOptions,
ConversationSearchOptions,
ConversationSearchResult,
ConversationTheme,
ArtifactOptions,
ConversationStats,
CompactionOptions,
CompactionResult
} from './conversation/types.js'

View file

@ -1,598 +0,0 @@
/**
* MCP Conversation Tools
*
* Exposes ConversationManager functionality through MCP for Claude Code integration.
* Provides 6 tools for infinite agent memory.
*
* REAL IMPLEMENTATION - Uses ConversationManager which uses real Brainy APIs
*/
import { v4 as uuidv4 } from '../universal/uuid.js'
import {
MCPResponse,
MCPToolExecutionRequest,
MCPTool,
MCP_VERSION
} from '../types/mcpTypes.js'
import { ConversationManager } from '../conversation/conversationManager.js'
import { Brainy } from '../brainy.js'
/**
* MCP Conversation Toolset
*
* Provides conversation and context management tools for AI agents
*/
export class MCPConversationToolset {
private conversationManager: ConversationManager
private initialized = false
/**
* Create MCP Conversation Toolset
* @param brain Brainy instance
*/
constructor(private brain: Brainy) {
this.conversationManager = new ConversationManager(brain)
}
/**
* Initialize the toolset
*/
async init(): Promise<void> {
if (this.initialized) {
return
}
await this.conversationManager.init()
this.initialized = true
}
/**
* Handle MCP tool execution request
* @param request MCP tool execution request
* @returns MCP response
*/
async handleRequest(request: MCPToolExecutionRequest): Promise<MCPResponse> {
if (!this.initialized) {
await this.init()
}
try {
const { toolName, parameters } = request
// Route to appropriate tool handler
switch (toolName) {
case 'conversation_save_message':
return await this.handleSaveMessage(request.requestId, parameters)
case 'conversation_get_context':
return await this.handleGetContext(request.requestId, parameters)
case 'conversation_search':
return await this.handleSearch(request.requestId, parameters)
case 'conversation_get_thread':
return await this.handleGetThread(request.requestId, parameters)
case 'conversation_save_artifact':
return await this.handleSaveArtifact(request.requestId, parameters)
case 'conversation_find_similar':
return await this.handleFindSimilar(request.requestId, parameters)
default:
return this.createErrorResponse(
request.requestId,
'UNKNOWN_TOOL',
`Unknown conversation tool: ${toolName}`
)
}
} catch (error) {
return this.createErrorResponse(
request.requestId,
'INTERNAL_ERROR',
error instanceof Error ? error.message : String(error)
)
}
}
/**
* Get available conversation tools
* @returns Array of MCP tool definitions
*/
async getAvailableTools(): Promise<MCPTool[]> {
return [
{
name: 'conversation_save_message',
description: 'Save a message to conversation history with automatic embedding and indexing',
parameters: {
type: 'object',
properties: {
content: {
type: 'string',
description: 'Message content'
},
role: {
type: 'string',
enum: ['user', 'assistant', 'system', 'tool'],
description: 'Message role'
},
conversationId: {
type: 'string',
description: 'Conversation ID (auto-generated if not provided)'
},
sessionId: {
type: 'string',
description: 'Session ID (optional)'
},
phase: {
type: 'string',
enum: [
'understanding',
'analysis',
'planning',
'implementation',
'testing',
'debugging',
'refinement',
'completed'
],
description: 'Problem-solving phase'
},
confidence: {
type: 'number',
minimum: 0,
maximum: 1,
description: 'Confidence score (0-1)'
},
artifacts: {
type: 'array',
items: { type: 'string' },
description: 'Artifact IDs or paths'
},
toolsUsed: {
type: 'array',
items: { type: 'string' },
description: 'Names of tools used'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Tags for categorization'
},
linkToPrevious: {
type: 'string',
description: 'ID of previous message to link'
}
},
required: ['content', 'role']
}
},
{
name: 'conversation_get_context',
description: 'Retrieve relevant context from conversation history using semantic search',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Query string for context retrieval'
},
conversationId: {
type: 'string',
description: 'Limit to specific conversation'
},
limit: {
type: 'number',
description: 'Maximum messages to return (default: 10)',
default: 10
},
maxTokens: {
type: 'number',
description: 'Token budget for context (default: 50000)',
default: 50000
},
relevanceThreshold: {
type: 'number',
minimum: 0,
maximum: 1,
description: 'Minimum similarity score (default: 0.7)',
default: 0.7
},
role: {
oneOf: [
{ type: 'string', enum: ['user', 'assistant', 'system', 'tool'] },
{ type: 'array', items: { type: 'string' } }
],
description: 'Filter by message role'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Filter by tags'
},
includeArtifacts: {
type: 'boolean',
description: 'Include linked artifacts',
default: false
},
includeSimilarConversations: {
type: 'boolean',
description: 'Include similar past conversations',
default: false
}
},
required: ['query']
}
},
{
name: 'conversation_search',
description: 'Search messages semantically across all conversations',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query'
},
limit: {
type: 'number',
description: 'Maximum results (default: 10)',
default: 10
},
conversationId: {
type: 'string',
description: 'Limit to specific conversation'
},
role: {
oneOf: [
{ type: 'string', enum: ['user', 'assistant', 'system', 'tool'] },
{ type: 'array', items: { type: 'string' } }
],
description: 'Filter by role'
},
timeRange: {
type: 'object',
properties: {
start: { type: 'number', description: 'Start timestamp' },
end: { type: 'number', description: 'End timestamp' }
},
description: 'Time range filter'
}
},
required: ['query']
}
},
{
name: 'conversation_get_thread',
description: 'Get full conversation thread with all messages',
parameters: {
type: 'object',
properties: {
conversationId: {
type: 'string',
description: 'Conversation ID'
},
includeArtifacts: {
type: 'boolean',
description: 'Include linked artifacts',
default: false
}
},
required: ['conversationId']
}
},
{
name: 'conversation_save_artifact',
description: 'Save code/file artifact and link to conversation',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'VFS path for artifact'
},
content: {
type: 'string',
description: 'Artifact content'
},
conversationId: {
type: 'string',
description: 'Conversation ID'
},
messageId: {
type: 'string',
description: 'Message ID to link artifact to'
},
type: {
type: 'string',
enum: ['code', 'config', 'data', 'document', 'other'],
description: 'Artifact type'
},
language: {
type: 'string',
description: 'Programming language (for code artifacts)'
},
description: {
type: 'string',
description: 'Artifact description'
}
},
required: ['path', 'content', 'conversationId']
}
},
{
name: 'conversation_find_similar',
description: 'Find similar past conversations using semantic similarity',
parameters: {
type: 'object',
properties: {
conversationId: {
type: 'string',
description: 'Conversation ID to find similar to'
},
limit: {
type: 'number',
description: 'Maximum results (default: 5)',
default: 5
},
threshold: {
type: 'number',
minimum: 0,
maximum: 1,
description: 'Minimum similarity threshold (default: 0.7)',
default: 0.7
}
},
required: ['conversationId']
}
}
]
}
/**
* Handle save_message tool
* REAL: Uses ConversationManager.saveMessage()
*/
private async handleSaveMessage(
requestId: string,
parameters: any
): Promise<MCPResponse> {
const {
content,
role,
conversationId,
sessionId,
phase,
confidence,
artifacts,
toolsUsed,
tags,
linkToPrevious
} = parameters
// Validate required parameters
if (!content || !role) {
return this.createErrorResponse(
requestId,
'INVALID_PARAMETERS',
'Missing required parameters: content and role are required'
)
}
// Save message (REAL)
const messageId = await this.conversationManager.saveMessage(content, role, {
conversationId,
sessionId,
phase,
confidence,
artifacts,
toolsUsed,
tags,
linkToPrevious
})
return this.createSuccessResponse(requestId, {
messageId,
conversationId: conversationId || messageId.split('_')[0]
})
}
/**
* Handle get_context tool
* REAL: Uses ConversationManager.getRelevantContext()
*/
private async handleGetContext(
requestId: string,
parameters: any
): Promise<MCPResponse> {
const { query, ...options } = parameters
if (!query) {
return this.createErrorResponse(
requestId,
'INVALID_PARAMETERS',
'Missing required parameter: query'
)
}
// Get context (REAL)
const context = await this.conversationManager.getRelevantContext(query, options)
return this.createSuccessResponse(requestId, context)
}
/**
* Handle search tool
* REAL: Uses ConversationManager.searchMessages()
*/
private async handleSearch(
requestId: string,
parameters: any
): Promise<MCPResponse> {
const { query } = parameters
if (!query) {
return this.createErrorResponse(
requestId,
'INVALID_PARAMETERS',
'Missing required parameter: query'
)
}
// Search messages (REAL)
const results = await this.conversationManager.searchMessages(parameters)
return this.createSuccessResponse(requestId, {
results,
count: results.length
})
}
/**
* Handle get_thread tool
* REAL: Uses ConversationManager.getConversationThread()
*/
private async handleGetThread(
requestId: string,
parameters: any
): Promise<MCPResponse> {
const { conversationId, includeArtifacts = false } = parameters
if (!conversationId) {
return this.createErrorResponse(
requestId,
'INVALID_PARAMETERS',
'Missing required parameter: conversationId'
)
}
// Get thread (REAL)
const thread = await this.conversationManager.getConversationThread(
conversationId,
{ includeArtifacts }
)
return this.createSuccessResponse(requestId, thread)
}
/**
* Handle save_artifact tool
* REAL: Uses ConversationManager.saveArtifact()
*/
private async handleSaveArtifact(
requestId: string,
parameters: any
): Promise<MCPResponse> {
const {
path,
content,
conversationId,
messageId,
type,
language,
description
} = parameters
if (!path || !content || !conversationId) {
return this.createErrorResponse(
requestId,
'INVALID_PARAMETERS',
'Missing required parameters: path, content, and conversationId are required'
)
}
// Save artifact (REAL)
const artifactId = await this.conversationManager.saveArtifact(path, content, {
conversationId,
messageId,
type,
language,
description
})
return this.createSuccessResponse(requestId, {
artifactId,
path
})
}
/**
* Handle find_similar tool
* REAL: Uses ConversationManager.findSimilarConversations()
*/
private async handleFindSimilar(
requestId: string,
parameters: any
): Promise<MCPResponse> {
const { conversationId, limit = 5, threshold = 0.7 } = parameters
if (!conversationId) {
return this.createErrorResponse(
requestId,
'INVALID_PARAMETERS',
'Missing required parameter: conversationId'
)
}
// Find similar (REAL)
const similar = await this.conversationManager.findSimilarConversations(
conversationId,
limit,
threshold
)
return this.createSuccessResponse(requestId, {
similar,
count: similar.length
})
}
/**
* Create success response
*/
private createSuccessResponse(requestId: string, data: any): MCPResponse {
return {
success: true,
requestId,
version: MCP_VERSION,
data
}
}
/**
* Create error response
*/
private createErrorResponse(
requestId: string,
code: string,
message: string,
details?: any
): MCPResponse {
return {
success: false,
requestId,
version: MCP_VERSION,
error: {
code,
message,
details
}
}
}
/**
* Generate request ID
*/
generateRequestId(): string {
return uuidv4()
}
}
/**
* Create MCP conversation toolset
* @param brain Brainy instance
* @returns MCPConversationToolset instance
*/
export function createConversationToolset(brain: Brainy): MCPConversationToolset {
return new MCPConversationToolset(brain)
}