Skip to main content
Glama

ACE MCP Server

playbook.tsโ€ข13.2 kB
/** * Playbook management system for the ACE framework. * * The Playbook stores and manages context bullets that guide LLM behavior. * It supports CRUD operations, filtering, search, and delta operations. */ import { v4 as uuidv4 } from 'uuid'; import { logger } from '../utils/logger'; import { ACEError } from '../utils/errors'; import { Bullet, DeltaOperation, PlaybookStats, BulletFilter, BulletSearchOptions, BulletSearchResult, ACEConfig } from './types'; import { DEFAULT_ACE_CONFIG } from './prompts'; /** * Error thrown by Playbook operations. */ export class PlaybookError extends ACEError { constructor(message: string, public readonly originalError?: Error) { super(message); this.name = 'PlaybookError'; Object.setPrototypeOf(this, PlaybookError.prototype); } } /** * Playbook manages a collection of context bullets. * * Features: * - In-memory storage with optional persistence * - Section-based organization * - Metadata tracking (helpful/harmful counts) * - Filtering and search capabilities * - Delta operation application * - Deduplication support */ export class Playbook { private bullets: Map<string, Bullet> = new Map(); private config: ACEConfig; constructor(config: Partial<ACEConfig> = {}) { this.config = { ...DEFAULT_ACE_CONFIG, ...config }; logger.info('Playbook initialized', { maxSize: this.config.maxPlaybookSize, dedupThreshold: this.config.dedupThreshold, defaultSections: this.config.defaultSections.length }); } /** * Add a new bullet to the playbook. */ addBullet(section: string, content: string, metadata?: Partial<Bullet['metadata']>): Bullet { if (this.bullets.size >= this.config.maxPlaybookSize) { throw new PlaybookError(`Playbook size limit reached (${this.config.maxPlaybookSize})`); } if (!section || !content.trim()) { throw new PlaybookError('Section and content are required'); } const bullet: Bullet = { id: uuidv4(), section: section.trim(), content: content.trim(), metadata: { created: new Date(), helpful_count: 0, harmful_count: 0, ...metadata } }; this.bullets.set(bullet.id, bullet); logger.debug('Bullet added', { id: bullet.id, section: bullet.section, contentLength: bullet.content.length }); return bullet; } /** * Get a bullet by ID. */ getBullet(id: string): Bullet | undefined { return this.bullets.get(id); } /** * Update an existing bullet. */ updateBullet(id: string, updates: Partial<Bullet>): Bullet { const bullet = this.bullets.get(id); if (!bullet) { throw new PlaybookError(`Bullet not found: ${id}`); } // Create updated bullet const updatedBullet: Bullet = { ...bullet, ...updates, id: bullet.id, // Preserve ID metadata: { ...bullet.metadata, ...updates.metadata } }; // Validate required fields if (!updatedBullet.section || !updatedBullet.content.trim()) { throw new PlaybookError('Section and content are required'); } this.bullets.set(id, updatedBullet); logger.debug('Bullet updated', { id, section: updatedBullet.section, contentLength: updatedBullet.content.length }); return updatedBullet; } /** * Delete a bullet by ID. */ deleteBullet(id: string): boolean { const existed = this.bullets.delete(id); if (existed) { logger.debug('Bullet deleted', { id }); } return existed; } /** * Get all bullets, optionally filtered. */ getBullets(filter?: BulletFilter): Bullet[] { let bullets = Array.from(this.bullets.values()); if (!filter) { return bullets; } // Apply filters if (filter.section) { bullets = bullets.filter(b => b.section === filter.section); } if (filter.min_helpful_count !== undefined) { bullets = bullets.filter(b => b.metadata.helpful_count >= filter.min_helpful_count!); } if (filter.max_harmful_count !== undefined) { bullets = bullets.filter(b => b.metadata.harmful_count <= filter.max_harmful_count!); } if (filter.content_contains) { const query = filter.content_contains.toLowerCase(); bullets = bullets.filter(b => b.content.toLowerCase().includes(query)); } if (filter.created_after) { bullets = bullets.filter(b => b.metadata.created >= filter.created_after!); } if (filter.created_before) { bullets = bullets.filter(b => b.metadata.created <= filter.created_before!); } if (filter.used_after && filter.used_after) { bullets = bullets.filter(b => b.metadata.last_used && b.metadata.last_used >= filter.used_after! ); } if (filter.used_before) { bullets = bullets.filter(b => b.metadata.last_used && b.metadata.last_used <= filter.used_before! ); } return bullets; } /** * Search bullets using text search and optional embeddings. */ async searchBullets(options: BulletSearchOptions): Promise<BulletSearchResult[]> { const { query, limit = 10, use_embeddings = false, min_similarity = 0.5 } = options; const results: BulletSearchResult[] = []; const queryLower = query.toLowerCase(); for (const bullet of this.bullets.values()) { const contentLower = bullet.content.toLowerCase(); // Exact match if (contentLower === queryLower) { results.push({ bullet, score: 1.0, match_type: 'exact' }); continue; } // Substring match if (contentLower.includes(queryLower)) { const score = queryLower.length / contentLower.length; results.push({ bullet, score: Math.min(score * 2, 0.9), // Boost substring matches but cap at 0.9 match_type: 'substring' }); continue; } // Embedding-based search (if available and requested) if (use_embeddings && bullet.metadata.embedding) { // Note: This would require query embedding, which needs LLM provider // For now, we'll skip embedding search in the base implementation // This can be enhanced when integrated with LLM providers } } // Sort by score descending and apply limit results.sort((a, b) => b.score - a.score); // Filter by minimum similarity const filtered = results.filter(r => r.score >= min_similarity); return filtered.slice(0, limit); } /** * Get bullets by section. */ getBulletsBySection(section: string): Bullet[] { return this.getBullets({ section }); } /** * Get all sections in the playbook. */ getSections(): string[] { const sections = new Set<string>(); for (const bullet of this.bullets.values()) { sections.add(bullet.section); } return Array.from(sections).sort(); } /** * Mark bullets as helpful or harmful based on trajectory feedback. */ updateBulletFeedback(bulletIds: string[], type: 'helpful' | 'harmful'): void { const now = new Date(); for (const id of bulletIds) { const bullet = this.bullets.get(id); if (bullet) { if (type === 'helpful') { bullet.metadata.helpful_count++; } else { bullet.metadata.harmful_count++; } bullet.metadata.last_used = now; logger.debug('Bullet feedback updated', { id, type, helpful_count: bullet.metadata.helpful_count, harmful_count: bullet.metadata.harmful_count }); } } } /** * Apply delta operations to the playbook. */ async applyDeltas(operations: DeltaOperation[]): Promise<void> { const results = { added: 0, updated: 0, deleted: 0, errors: 0 }; for (const operation of operations) { try { switch (operation.type) { case 'ADD': if (!operation.bullet) { throw new PlaybookError('ADD operation requires bullet data'); } // Generate new ID if not provided const bulletToAdd = { ...operation.bullet, id: operation.bullet.id || uuidv4() }; this.bullets.set(bulletToAdd.id, bulletToAdd); results.added++; break; case 'UPDATE': if (!operation.bulletId || !operation.updates) { throw new PlaybookError('UPDATE operation requires bulletId and updates'); } this.updateBullet(operation.bulletId, operation.updates); results.updated++; break; case 'DELETE': if (!operation.bulletId) { throw new PlaybookError('DELETE operation requires bulletId'); } const deleted = this.deleteBullet(operation.bulletId); if (deleted) { results.deleted++; } break; default: throw new PlaybookError(`Unknown operation type: ${(operation as any).type}`); } } catch (error) { results.errors++; logger.error('Delta operation failed', { operation: operation.type, bulletId: operation.bulletId, error: (error as Error).message }); } } logger.info('Delta operations applied', results); } /** * Get playbook statistics. */ getStats(): PlaybookStats { const bullets = Array.from(this.bullets.values()); if (bullets.length === 0) { return { total_bullets: 0, bullets_by_section: {}, avg_helpful_count: 0, avg_harmful_count: 0 }; } // Count bullets by section const bullets_by_section: Record<string, number> = {}; for (const bullet of bullets) { bullets_by_section[bullet.section] = (bullets_by_section[bullet.section] || 0) + 1; } // Calculate averages const totalHelpful = bullets.reduce((sum, b) => sum + b.metadata.helpful_count, 0); const totalHarmful = bullets.reduce((sum, b) => sum + b.metadata.harmful_count, 0); // Find most recent and most helpful bullets const mostRecent = bullets .filter(b => b.metadata.last_used) .sort((a, b) => (b.metadata.last_used?.getTime() || 0) - (a.metadata.last_used?.getTime() || 0))[0]; const mostHelpful = bullets .sort((a, b) => b.metadata.helpful_count - a.metadata.helpful_count)[0]; return { total_bullets: bullets.length, bullets_by_section, avg_helpful_count: totalHelpful / bullets.length, avg_harmful_count: totalHarmful / bullets.length, most_recent_bullet: mostRecent, most_helpful_bullet: mostHelpful }; } /** * Get the total number of bullets. */ getBulletCount(): number { return this.bullets.size; } /** * Clear all bullets from the playbook. */ clear(): void { const count = this.bullets.size; this.bullets.clear(); logger.info('Playbook cleared', { bulletCount: count }); } /** * Export playbook data for persistence. */ export(): Bullet[] { return Array.from(this.bullets.values()); } /** * Import playbook data from persistence. */ import(bullets: Bullet[]): void { this.bullets.clear(); for (const bullet of bullets) { // Validate bullet structure if (!bullet.id || !bullet.section || !bullet.content || !bullet.metadata) { logger.warn('Skipping invalid bullet during import', { bullet }); continue; } this.bullets.set(bullet.id, bullet); } logger.info('Playbook imported', { bulletCount: this.bullets.size }); } /** * Calculate cosine similarity between two embedding vectors. */ static cosineSimilarity(a: number[], b: number[]): number { if (a.length !== b.length) { throw new PlaybookError('Embedding vectors must have the same length'); } const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0); const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); if (magnitudeA === 0 || magnitudeB === 0) { return 0; } return dotProduct / (magnitudeA * magnitudeB); } /** * Find similar bullets using embedding similarity. */ findSimilarBullets(embedding: number[], threshold: number = this.config.dedupThreshold): Bullet[] { const similar: Bullet[] = []; for (const bullet of this.bullets.values()) { if (bullet.metadata.embedding) { const similarity = Playbook.cosineSimilarity(embedding, bullet.metadata.embedding); if (similarity >= threshold) { similar.push(bullet); } } } return similar.sort((a, b) => { const simA = Playbook.cosineSimilarity(embedding, a.metadata.embedding!); const simB = Playbook.cosineSimilarity(embedding, b.metadata.embedding!); return simB - simA; }); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Angry-Robot-Deals/ace-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server