contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
430 lines (425 loc) • 17.5 kB
JavaScript
import { BaseTool } from './BaseTool.js';
import { FileService } from '../fileService.js';
export class MemoryTool extends BaseTool {
constructor(baseDir) {
super();
this.maxMemoryEntries = 50; // Limit to prevent memory from growing too large
this.fileService = new FileService(baseDir);
this.memoryFile = '.chat_memory.txt';
}
getName() {
return 'memory';
}
getDescription() {
return 'Manage chat memory to maintain context across long conversations. Use this to store important information, track progress, and maintain continuity.';
}
getParameters() {
return [
{
name: 'operation',
type: 'string',
description: 'Memory operation: read, append, update, clear, summarize',
required: true
},
{
name: 'content',
type: 'string',
description: 'Content to store in memory (required for append/update operations)',
required: false
},
{
name: 'max_entries',
type: 'number',
description: 'Maximum number of memory entries to keep (default: 50)',
required: false
}
];
}
getAgentGuidance() {
return `**Memory Tool Usage Guidelines:**
Use the memory tool to maintain context across long conversations:
1. **After completing significant tasks**: Store key outcomes and decisions
2. **When starting new topics**: Read memory to understand previous context
3. **End-of-turn memory**: You can include memory calls with your final response - they won't trigger additional LLM turns
4. **Tool results are automatically stored**: Non-memory tool results are automatically saved to memory
**Best Practices:**
- Keep entries concise but informative
- Include key decisions, completed tasks, and important context
- Use present tense and be specific
- Focus on information that will be useful in future conversations
- Memory calls at the end of responses are processed without additional turns
**Example Usage:**
\`\`\`xml
<tool_call name="memory" id="1">
<operation>append</operation>
<content>User requested screenplay agent modification. Successfully updated PromptTemplates.ts to transform chat agent into cinematic screenplay creation assistant. Test completed - agent now creates story outlines, character profiles, and scene files with visual sketches.</content>
</tool_call>
\`\`\``;
}
async execute(parameters) {
const validationError = this.validateParameters(parameters);
if (validationError) {
return validationError;
}
const { operation, content, max_entries } = parameters;
if (max_entries && typeof max_entries === 'number') {
this.maxMemoryEntries = max_entries;
}
try {
switch (operation) {
case 'read':
return await this.readMemory();
case 'append':
if (!content) {
return {
success: false,
error: 'Content is required for append operation'
};
}
return await this.appendMemory(content);
case 'update':
if (!content) {
return {
success: false,
error: 'Content is required for update operation'
};
}
return await this.updateMemory(content);
case 'clear':
return await this.clearMemory();
case 'summarize':
return await this.summarizeMemory();
default:
return {
success: false,
error: `Unknown operation: ${operation}. Available operations: read, append, update, clear, summarize`
};
}
}
catch (error) {
return {
success: false,
error: `Memory operation failed: ${error.message}`
};
}
}
async readMemory() {
try {
const content = await this.fileService.readFile(this.memoryFile);
const entries = this.parseMemoryContent(content);
return {
success: true,
data: {
entries,
total_entries: entries.length,
memory_file: this.memoryFile
},
message: `Memory contains ${entries.length} entries`
};
}
catch (error) {
// Memory file doesn't exist yet
return {
success: true,
data: {
entries: [],
total_entries: 0,
memory_file: this.memoryFile
},
message: 'Memory is empty (no memory file found)'
};
}
}
async appendMemory(content) {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
const newEntry = `[${timestamp}] ${content}`;
try {
// Read existing memory
let existingContent = '';
try {
existingContent = await this.fileService.readFile(this.memoryFile);
}
catch (error) {
// File doesn't exist, start fresh
}
// Parse existing entries
const entries = this.parseMemoryContent(existingContent);
entries.push({ timestamp, content });
// Trim to max entries if needed
const trimmedEntries = entries.slice(-this.maxMemoryEntries);
// Write back to file
const newContent = trimmedEntries
.map(entry => `[${entry.timestamp}] ${entry.content}`)
.join('\n');
await this.fileService.saveFile({
path: this.memoryFile,
content: newContent
});
return {
success: true,
data: {
new_entry: newEntry,
total_entries: trimmedEntries.length,
trimmed: entries.length > this.maxMemoryEntries
},
message: `Added memory entry. Total entries: ${trimmedEntries.length}${entries.length > this.maxMemoryEntries ? ' (trimmed older entries)' : ''}`
};
}
catch (error) {
return {
success: false,
error: `Failed to append memory: ${error.message}`
};
}
}
async updateMemory(content) {
try {
await this.fileService.saveFile({
path: this.memoryFile,
content: content
});
const entries = this.parseMemoryContent(content);
return {
success: true,
data: {
total_entries: entries.length,
memory_file: this.memoryFile
},
message: `Memory updated with ${entries.length} entries`
};
}
catch (error) {
return {
success: false,
error: `Failed to update memory: ${error.message}`
};
}
}
async clearMemory() {
try {
await this.fileService.saveFile({
path: this.memoryFile,
content: ''
});
return {
success: true,
data: {
memory_file: this.memoryFile
},
message: 'Memory cleared successfully'
};
}
catch (error) {
return {
success: false,
error: `Failed to clear memory: ${error.message}`
};
}
}
async summarizeMemory() {
try {
const content = await this.fileService.readFile(this.memoryFile);
const entries = this.parseMemoryContent(content);
if (entries.length <= 10) {
return {
success: true,
data: {
total_entries: entries.length,
action: 'no_summarization_needed'
},
message: 'Memory has few entries, no summarization needed'
};
}
// Keep recent entries and summarize older ones
const recentEntries = entries.slice(-10);
const olderEntries = entries.slice(0, -10);
// Create a simple summary of older entries
const summary = this.createSummary(olderEntries);
const summaryEntry = {
timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19),
content: `SUMMARY: ${summary}`
};
// Combine summary with recent entries
const newEntries = [summaryEntry, ...recentEntries];
const newContent = newEntries
.map(entry => `[${entry.timestamp}] ${entry.content}`)
.join('\n');
await this.fileService.saveFile({
path: this.memoryFile,
content: newContent
});
return {
success: true,
data: {
original_entries: entries.length,
new_entries: newEntries.length,
summarized_entries: olderEntries.length
},
message: `Memory summarized: ${olderEntries.length} older entries condensed into summary, ${recentEntries.length} recent entries preserved`
};
}
catch (error) {
return {
success: false,
error: `Failed to summarize memory: ${error.message}`
};
}
}
parseMemoryContent(content) {
if (!content.trim()) {
return [];
}
return content
.split('\n')
.filter(line => line.trim())
.map(line => {
const match = line.match(/^\[([^\]]+)\] (.+)$/);
if (match) {
return {
timestamp: match[1],
content: match[2]
};
}
// Fallback for malformed entries
return {
timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19),
content: line
};
});
}
createSummary(entries) {
// Simple summarization - extract key topics and actions
const topics = new Set();
const actions = new Set();
entries.forEach(entry => {
const content = entry.content.toLowerCase();
// Extract potential topics (simple keyword matching)
if (content.includes('screenplay') || content.includes('script'))
topics.add('screenplay development');
if (content.includes('agent') || content.includes('chat'))
topics.add('chat agent configuration');
if (content.includes('test') || content.includes('demo'))
topics.add('testing and validation');
if (content.includes('file') || content.includes('tool'))
topics.add('file operations');
if (content.includes('memory') || content.includes('context'))
topics.add('memory management');
// Extract actions
if (content.includes('created') || content.includes('generated'))
actions.add('content creation');
if (content.includes('modified') || content.includes('updated'))
actions.add('system modifications');
if (content.includes('completed') || content.includes('finished'))
actions.add('task completion');
});
const topicList = Array.from(topics).join(', ');
const actionList = Array.from(actions).join(', ');
return `Previous conversation covered: ${topicList}. Key actions: ${actionList}. (${entries.length} entries from ${entries[0]?.timestamp} to ${entries[entries.length - 1]?.timestamp})`;
}
/**
* Get current memory content for system prompt inclusion
*/
async getMemoryForSystemPrompt() {
try {
const result = await this.readMemory();
if (result.success && result.data.entries.length > 0) {
const entries = result.data.entries;
const recentEntries = entries.slice(-10); // Only include recent entries in system prompt
return recentEntries
.map(entry => `[${entry.timestamp}] ${entry.content}`)
.join('\n');
}
return '';
}
catch (error) {
return '';
}
}
/**
* Automatically store tool response in memory
*/
async storeToolResponse(toolName, toolResult, parameters) {
try {
const summary = this.summarizeToolResult(toolName, toolResult, parameters);
await this.appendMemory(`TOOL_RESULT: ${toolName} - ${summary}`);
}
catch (error) {
// Silently fail - memory storage shouldn't break the main flow
console.error('Failed to store tool response in memory:', error);
}
}
/**
* Store conversation summary when trimming history
*/
async storeConversationSummary(messages) {
try {
const summary = this.summarizeConversation(messages);
if (summary) {
await this.appendMemory(`CONVERSATION_SUMMARY: ${summary}`);
}
}
catch (error) {
console.error('Failed to store conversation summary:', error);
}
}
summarizeToolResult(toolName, toolResult, parameters) {
const status = toolResult.success ? 'SUCCESS' : 'FAILED';
// Create concise summaries based on tool type
switch (toolName) {
case 'write_file':
const operation = parameters?.operation_type || 'write';
const filePath = parameters?.file_path || 'unknown';
return `${status} - ${operation} file ${filePath}`;
case 'read_file':
const readPath = parameters?.file_path || 'unknown';
return `${status} - read file ${readPath}`;
case 'execute_command':
const command = parameters?.command || 'unknown command';
return `${status} - executed: ${command.substring(0, 50)}${command.length > 50 ? '...' : ''}`;
case 'image_generation':
return `${status} - generated image`;
case 'audio_generation':
return `${status} - generated audio`;
default:
return `${status} - ${toolResult.message || 'completed'}`;
}
}
summarizeConversation(messages) {
if (messages.length === 0)
return '';
// Extract key topics and actions from conversation
const userMessages = messages.filter(m => m.role === 'user');
const assistantMessages = messages.filter(m => m.role === 'assistant');
if (userMessages.length === 0)
return '';
// Simple summarization - extract key topics
const topics = new Set();
const actions = new Set();
[...userMessages, ...assistantMessages].forEach(msg => {
const content = msg.content.toLowerCase();
// Extract topics
if (content.includes('screenplay') || content.includes('script'))
topics.add('screenplay');
if (content.includes('agent') || content.includes('chat'))
topics.add('agent');
if (content.includes('memory') || content.includes('context'))
topics.add('memory');
if (content.includes('file') || content.includes('tool'))
topics.add('files');
if (content.includes('test') || content.includes('demo'))
topics.add('testing');
// Extract actions
if (content.includes('create') || content.includes('generate'))
actions.add('creation');
if (content.includes('modify') || content.includes('update'))
actions.add('modification');
if (content.includes('read') || content.includes('analyze'))
actions.add('analysis');
});
const topicList = Array.from(topics).join(', ');
const actionList = Array.from(actions).join(', ');
const startTime = messages[0]?.timestamp ? new Date(messages[0].timestamp).toISOString().substring(0, 19) : 'unknown';
const endTime = messages[messages.length - 1]?.timestamp ? new Date(messages[messages.length - 1].timestamp).toISOString().substring(0, 19) : 'unknown';
return `Conversation (${startTime} to ${endTime}): Topics: ${topicList || 'general'}. Actions: ${actionList || 'discussion'}. (${messages.length} messages)`;
}
}