diff --git a/ROADMAP.md b/ROADMAP.md index 2af1f0c..42fee61 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -16,6 +16,6 @@ ## Medium Priority -- [ ] `ContextManager` implementation (attention/relevance filtering). +- [x] `ContextManager` implementation (attention/relevance filtering). (Done: `ReferenceContextManager` + `ContextManagerModule` + demo) - [ ] `CapabilitiesManager` for tool pre-selection. - [ ] Define Sensor and Actuator capability contracts. diff --git a/packages/reference-implementation/package.json b/packages/reference-implementation/package.json index 083c25f..e7a9c54 100644 --- a/packages/reference-implementation/package.json +++ b/packages/reference-implementation/package.json @@ -8,7 +8,9 @@ "build": "tsc", "test": "node --test __tests__/*.test.js", "demo": "tsx src/demo.ts", - "dev": "tsx --watch src/demo.ts" + "demo:context": "tsx src/context-demo.ts", + "dev": "tsx --watch src/demo.ts", + "dev:context": "tsx --watch src/context-demo.ts" }, "keywords": ["ami", "cognitive", "implementation"], "author": "", diff --git a/packages/reference-implementation/src/__tests__/context-manager.test.ts b/packages/reference-implementation/src/__tests__/context-manager.test.ts new file mode 100644 index 0000000..437385d --- /dev/null +++ b/packages/reference-implementation/src/__tests__/context-manager.test.ts @@ -0,0 +1,251 @@ +/** + * Tests for ReferenceContextManager + */ + +import test from 'node:test'; +import assert from 'node:assert'; +import { ReferenceContextManager, type ContextWindow } from '../context-manager.js'; +import type { Message, Fact } from '@ami/skeleton'; + +test('ReferenceContextManager', async (t) => { + await t.test('initialization', async () => { + const contextManager = new ReferenceContextManager(); + const window = contextManager.getCurrentContext(); + + assert.strictEqual(window.recentMessages.length, 0, 'should start with empty messages'); + assert.strictEqual(window.relevantFacts.length, 0, 'should start with empty facts'); + assert.strictEqual(window.focusTopic, undefined, 'should start with no focus topic'); + }); + + await t.test('updateContext with messages', async () => { + const contextManager = new ReferenceContextManager({ + maxMessages: 3, + relevanceThreshold: 0.2, + recencyWindow: 60000 + }); + + const now = Date.now(); + const messages: Message[] = [ + { + role: 'user', + content: 'Let me tell you about machine learning', + timestamp: now - 1000 + }, + { + role: 'assistant', + content: 'That sounds interesting! Tell me more about neural networks.', + timestamp: now - 500 + }, + { + role: 'user', + content: 'What\'s the weather like today?', + timestamp: now + } + ]; + + await contextManager.updateContext(messages, []); + const window = contextManager.getCurrentContext(); + + assert.strictEqual(window.recentMessages.length, 3, 'should include all messages within limit'); + assert.ok(window.lastUpdated > 0, 'should update timestamp'); + }); + + await t.test('message filtering by recency', async () => { + const contextManager = new ReferenceContextManager({ + maxMessages: 5, + recencyWindow: 30000 // 30 seconds + }); + + const now = Date.now(); + const messages: Message[] = [ + { + role: 'user', + content: 'Old message', + timestamp: now - 60000 // 1 minute ago (too old) + }, + { + role: 'user', + content: 'Recent message', + timestamp: now - 10000 // 10 seconds ago (within window) + } + ]; + + await contextManager.updateContext(messages, []); + const window = contextManager.getCurrentContext(); + + assert.strictEqual(window.recentMessages.length, 1, 'should filter old messages'); + assert.strictEqual(window.recentMessages[0].content, 'Recent message', 'should keep recent message'); + }); + + await t.test('message filtering by relevance threshold', async () => { + const contextManager = new ReferenceContextManager({ + maxMessages: 5, + relevanceThreshold: 0.5 // High threshold + }); + + const messages: Message[] = [ + { + role: 'user', + content: 'ai machine learning neural networks deep learning', // Should be relevant + timestamp: Date.now() + }, + { + role: 'user', + content: 'hello', // Should be less relevant + timestamp: Date.now() + } + ]; + + await contextManager.updateContext(messages, []); + const window = contextManager.getCurrentContext(); + + // With high threshold, should filter out low-relevance messages + assert.ok(window.recentMessages.length <= 2, 'should apply relevance filtering'); + }); + + await t.test('fact filtering and inclusion', async () => { + const contextManager = new ReferenceContextManager({ + maxFacts: 2, + relevanceThreshold: 0.1 + }); + + const facts: Fact[] = [ + { + id: 'fact1', + text: 'Neural networks are a subset of machine learning algorithms', + relations: [], + timestamp: Date.now() + }, + { + id: 'fact2', + text: 'The weather today is sunny and warm', + relations: [], + timestamp: Date.now() + }, + { + id: 'fact3', + text: 'Deep learning uses multiple layers in neural networks', + relations: ['fact1'], + timestamp: Date.now() + } + ]; + + const messages: Message[] = [ + { + role: 'user', + content: 'Tell me about artificial intelligence and neural networks', + timestamp: Date.now() + } + ]; + + await contextManager.updateContext(messages, facts); + const window = contextManager.getCurrentContext(); + + assert.strictEqual(window.relevantFacts.length, 2, 'should respect maxFacts limit'); + assert.ok(window.relevantFacts.length > 0, 'should include some facts'); + }); + + await t.test('focus topic extraction', async () => { + const contextManager = new ReferenceContextManager(); + + const messages: Message[] = [ + { + role: 'user', + content: 'Let me ask about machine learning algorithms and neural networks', + timestamp: Date.now() + }, + { + role: 'assistant', + content: 'Neural networks are indeed fascinating machine learning models', + timestamp: Date.now() + } + ]; + + await contextManager.updateContext(messages, []); + const window = contextManager.getCurrentContext(); + + assert.ok(window.focusTopic, 'should extract focus topic'); + assert.ok( + window.focusTopic!.toLowerCase().includes('machine') || + window.focusTopic!.toLowerCase().includes('neural') || + window.focusTopic!.toLowerCase().includes('learning'), + 'focus topic should reflect conversation content' + ); + }); + + await t.test('calculateRelevance scoring', async () => { + const contextManager = new ReferenceContextManager(); + + const message: Message = { + role: 'user', + content: 'Tell me about artificial intelligence and machine learning', + timestamp: Date.now() + }; + + const fact: Fact = { + id: 'fact1', + text: 'Machine learning is a subset of artificial intelligence', + relations: [], + timestamp: Date.now() + }; + + const messageScore = contextManager.calculateRelevance(message, 'artificial intelligence'); + const factScore = contextManager.calculateRelevance(fact, 'machine learning'); + + assert.ok(messageScore.score > 0, 'should score relevant message positively'); + assert.ok(factScore.score > 0, 'should score relevant fact positively'); + assert.ok(messageScore.reasons.length > 0, 'should provide reasoning for message score'); + assert.ok(factScore.reasons.length > 0, 'should provide reasoning for fact score'); + }); + + await t.test('context window limits', async () => { + const contextManager = new ReferenceContextManager({ + maxMessages: 2, + maxFacts: 1 + }); + + const messages: Message[] = Array.from({ length: 5 }, (_, i) => ({ + role: 'user', + content: `Message ${i + 1} about important topics`, + timestamp: Date.now() - i * 1000 + })); + + const facts: Fact[] = Array.from({ length: 3 }, (_, i) => ({ + id: `fact${i + 1}`, + text: `Fact ${i + 1} about relevant information`, + relations: [], + timestamp: Date.now() + })); + + await contextManager.updateContext(messages, facts); + const window = contextManager.getCurrentContext(); + + assert.strictEqual(window.recentMessages.length, 2, 'should respect maxMessages limit'); + assert.strictEqual(window.relevantFacts.length, 1, 'should respect maxFacts limit'); + }); + + await t.test('empty input handling', async () => { + const contextManager = new ReferenceContextManager(); + + // Test with empty arrays + await contextManager.updateContext([], []); + let window = contextManager.getCurrentContext(); + + assert.strictEqual(window.recentMessages.length, 0, 'should handle empty messages'); + assert.strictEqual(window.relevantFacts.length, 0, 'should handle empty facts'); + + // Test with undefined/null handling gracefully + const messages: Message[] = [ + { + role: 'user', + content: '', + timestamp: Date.now() + } + ]; + + await contextManager.updateContext(messages, []); + window = contextManager.getCurrentContext(); + + assert.ok(window.lastUpdated > 0, 'should still update timestamp with empty content'); + }); +}); \ No newline at end of file diff --git a/packages/reference-implementation/src/context-demo.ts b/packages/reference-implementation/src/context-demo.ts new file mode 100644 index 0000000..d332cad --- /dev/null +++ b/packages/reference-implementation/src/context-demo.ts @@ -0,0 +1,188 @@ +#!/usr/bin/env node +/** + * Demo: ContextManager integration with the cognitive system. + * + * This demonstrates the attention/relevance filtering capabilities: + * 1. ContextManager processes incoming perceptions + * 2. Maintains focused attention window + * 3. Emits context.changed events + * 4. Works alongside the knowledge distiller + * + * Usage: + * npx tsx context-demo.ts + * npm run demo:context + */ + +import { ReferenceCognitiveRegistry } from './cognitive-registry.js'; +import { ReferenceCognitiveBus } from './cognitive-bus.js'; +import { DistillerModule } from './distiller-module.js'; +import { ContextManagerModule } from './context-manager-module.js'; +import { PatternExtractionStrategy } from './strategies/index.js'; +import type { Message, Fact } from '@ami/skeleton'; +import type { ContextWindow } from './context-manager.js'; + +async function runContextDemo(): Promise { + console.log('🧠 AMI ContextManager Demo'); + console.log('============================\n'); + + // 1. Create the cognitive infrastructure + const bus = new ReferenceCognitiveBus(); + const registry = new ReferenceCognitiveRegistry(bus); + + // 2. Register modules + console.log('šŸ”§ Registering cognitive modules...'); + + const distillerModule = new DistillerModule({ + strategy: new PatternExtractionStrategy() + }); + registry.register(distillerModule); + console.log(` āœ… Registered: ${distillerModule.name}`); + + const contextModule = new ContextManagerModule({ + maxMessages: 8, + maxFacts: 5, + relevanceThreshold: 0.2, + recencyWindow: 300000, // 5 minutes + updateThrottleMs: 500 + }); + registry.register(contextModule); + console.log(` āœ… Registered: ${contextModule.name}`); + + // 3. Subscribe to context changes + let contextChangeCount = 0; + bus.on('context.changed', (event) => { + contextChangeCount++; + console.log(`\nšŸ“” Context changed (#${contextChangeCount}):`); + console.log(` šŸŽÆ Focus topic: ${event.payload.focusTopic || 'none'}`); + console.log(` šŸ’­ Recent messages: ${event.payload.recentMessages.length}`); + console.log(` 🧠 Relevant facts: ${event.payload.relevantFacts.length}`); + + if (event.payload.focusTopic) { + const recentTopics = event.payload.recentMessages + .slice(-3) + .map((m: Message) => `"${m.content.substring(0, 40)}..."`); + console.log(` šŸ“ Recent: [${recentTopics.join(', ')}]`); + } + }); + + // Subscribe to fact creation + let factCount = 0; + bus.on('fact.created', (event) => { + factCount++; + console.log(`\nšŸ”¬ Fact created (#${factCount}): "${event.payload.text.substring(0, 60)}..."`); + }); + + // 4. Initialize all modules + console.log('\nšŸš€ Initializing cognitive system...'); + await registry.initAll({}); + + // 5. Simulate a conversation about TypeScript + console.log('\nšŸ’¬ Simulating TypeScript conversation...'); + + const messages: Message[] = [ + { + role: 'user', + content: 'I want to learn about TypeScript', + timestamp: Date.now(), + }, + { + role: 'assistant', + content: 'TypeScript is a superset of JavaScript that adds static typing', + timestamp: Date.now() + 1000, + }, + { + role: 'user', + content: 'What are generics in TypeScript?', + timestamp: Date.now() + 2000, + }, + { + role: 'assistant', + content: 'Generics allow you to create reusable components that work with multiple types', + timestamp: Date.now() + 3000, + }, + { + role: 'user', + content: 'Can you show me an example of TypeScript interfaces?', + timestamp: Date.now() + 4000, + }, + ]; + + for (const message of messages) { + console.log(`\nšŸ“„ Processing: "${message.content.substring(0, 50)}..."`); + bus.emit('perception.text', message); + await new Promise(resolve => setTimeout(resolve, 800)); // Delay for demo + } + + // 6. Change topic to show context shifting + console.log('\nšŸ”„ Shifting topic to machine learning...'); + + const newTopicMessages: Message[] = [ + { + role: 'user', + content: 'Now tell me about machine learning algorithms', + timestamp: Date.now() + 5000, + }, + { + role: 'assistant', + content: 'Machine learning involves training models on data to make predictions', + timestamp: Date.now() + 6000, + }, + { + role: 'user', + content: 'What is the difference between supervised and unsupervised learning?', + timestamp: Date.now() + 7000, + }, + ]; + + for (const message of newTopicMessages) { + console.log(`\nšŸ“„ Processing: "${message.content.substring(0, 50)}..."`); + bus.emit('perception.text', message); + await new Promise(resolve => setTimeout(resolve, 800)); + } + + // 7. Trigger manual context update with batch + console.log('\nšŸ”„ Manual context update with all messages...'); + bus.emit('context.update', { + messages: [...messages, ...newTopicMessages] + }); + + // 8. Wait for processing + console.log('\nā³ Waiting for cognitive processing...'); + await new Promise(resolve => setTimeout(resolve, 2000)); + + // 9. Show final context state + const finalContext = contextModule.getCurrentContext(); + if (finalContext) { + console.log('\nšŸ“Š Final context state:'); + console.log(` šŸŽÆ Focus: ${finalContext.focusTopic}`); + console.log(` šŸ’­ Messages: ${finalContext.recentMessages.length}`); + console.log(` 🧠 Facts: ${finalContext.relevantFacts.length}`); + console.log(` ā° Last updated: ${new Date(finalContext.lastUpdated).toISOString()}`); + } + + // 10. Graceful shutdown + console.log('\nšŸ›‘ Shutting down cognitive system...'); + await registry.destroyAll(); + + console.log('\nāœ… ContextManager demo completed! šŸŽ‰'); + console.log(`šŸ“ˆ Statistics: ${contextChangeCount} context changes, ${factCount} facts created`); +} + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\nšŸ›‘ Received SIGINT, shutting down gracefully...'); + process.exit(0); +}); + +process.on('SIGTERM', () => { + console.log('\nšŸ›‘ Received SIGTERM, shutting down gracefully...'); + process.exit(0); +}); + +// Run the demo +if (import.meta.url === new URL(process.argv[1], 'file://').href) { + runContextDemo().catch(error => { + console.error('āŒ Context demo failed:', error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/reference-implementation/src/context-manager-module.ts b/packages/reference-implementation/src/context-manager-module.ts new file mode 100644 index 0000000..42fca65 --- /dev/null +++ b/packages/reference-implementation/src/context-manager-module.ts @@ -0,0 +1,192 @@ +/** + * ContextManagerModule — ContextManager wrapped as a CognitiveModule. + * + * Subscribes to perception events and fact updates, maintains focused + * attention window, and emits `context.changed` events when focus shifts. + * + * This demonstrates how attention/relevance filtering integrates with + * the capability-based OS architecture (DEC-003). + * + * @see ROADMAP.md — "ContextManager implementation (attention/relevance filtering)" + */ + +import type { + CognitiveBus, + CognitiveCapability, + CognitiveModule, + Message, + Fact, +} from '@ami/skeleton'; + +import { ReferenceContextManager } from './context-manager.js'; +import type { ContextManagerConfig, ContextWindow } from './context-manager.js'; + +export interface ContextManagerModuleConfig extends ContextManagerConfig { + /** Whether to auto-update context on incoming messages */ + autoUpdate?: boolean; + /** Minimum time between context updates (ms) */ + updateThrottleMs?: number; +} + +export class ContextManagerModule implements CognitiveModule { + readonly id = 'processor.context'; + readonly name = 'Context Manager'; + readonly capabilities: CognitiveCapability[] = ['processor']; + + status: CognitiveModule['status'] = 'registered'; + + private bus: CognitiveBus | null = null; + private contextManager: ReferenceContextManager | null = null; + private readonly moduleConfig: ContextManagerModuleConfig; + private readonly factsBuffer: Fact[] = []; + private lastUpdateTime = 0; + + constructor(config: ContextManagerModuleConfig = {}) { + this.moduleConfig = { + autoUpdate: config.autoUpdate ?? true, + updateThrottleMs: config.updateThrottleMs ?? 1000, + ...config + }; + } + + async init(bus: CognitiveBus, _config: Record): Promise { + this.status = 'initializing'; + + this.bus = bus; + this.contextManager = new ReferenceContextManager(this.moduleConfig); + + // Subscribe to inputs that affect context + if (this.moduleConfig.autoUpdate) { + bus.on('perception.text', this.handleTextPerception); + bus.on('perception.audio', this.handleAudioPerception); + bus.on('fact.created', this.handleNewFact); + bus.on('fact.updated', this.handleUpdatedFact); + } + + // Subscribe to manual context update requests + bus.on<{ messages?: Message[]; facts?: Fact[] }>('context.update', this.handleContextUpdateRequest); + + this.status = 'ready'; + bus.emit('module.ready', { moduleId: this.id }); + } + + async destroy(): Promise { + if (this.bus) { + this.bus.off('perception.text', this.handleTextPerception); + this.bus.off('perception.audio', this.handleAudioPerception); + this.bus.off('fact.created', this.handleNewFact); + this.bus.off('fact.updated', this.handleUpdatedFact); + this.bus.off('context.update', this.handleContextUpdateRequest); + } + this.bus = null; + this.contextManager = null; + this.status = 'stopped'; + } + + /** + * Get current context window. + */ + getCurrentContext(): ContextWindow | null { + return this.contextManager?.getCurrentContext() ?? null; + } + + /** + * Handle text perception (user input, etc.) + */ + private handleTextPerception = async (event: { payload: Message }): Promise => { + await this.processNewMessage(event.payload); + }; + + /** + * Handle audio perception (transcribed speech, etc.) + */ + private handleAudioPerception = async (event: { payload: Message }): Promise => { + await this.processNewMessage(event.payload); + }; + + /** + * Handle new fact creation from distillation + */ + private handleNewFact = async (event: { payload: Fact }): Promise => { + this.factsBuffer.push(event.payload); + await this.maybeUpdateContext(); + }; + + /** + * Handle fact updates + */ + private handleUpdatedFact = async (event: { payload: Fact }): Promise => { + // Replace existing fact in buffer or add if not present + const existingIndex = this.factsBuffer.findIndex(f => f.id === event.payload.id); + if (existingIndex >= 0) { + this.factsBuffer[existingIndex] = event.payload; + } else { + this.factsBuffer.push(event.payload); + } + await this.maybeUpdateContext(); + }; + + /** + * Handle manual context update requests + */ + private handleContextUpdateRequest = async (event: { + payload: { messages?: Message[]; facts?: Fact[] } + }): Promise => { + if (!this.contextManager || !this.bus) return; + + try { + const { messages = [], facts } = event.payload; + + // If facts provided, update buffer + if (facts) { + this.factsBuffer.splice(0, this.factsBuffer.length, ...facts); + } + + const result = await this.contextManager.updateContext(messages, this.factsBuffer); + + if (result.contextChanged) { + this.bus.emit('context.changed', result.context); + } + + this.lastUpdateTime = Date.now(); + } catch (err) { + console.error(`[ContextManagerModule] Manual context update failed:`, err); + this.status = 'degraded'; + this.bus?.emit('module.degraded', { + moduleId: this.id, + reason: String(err), + }); + } + }; + + private async processNewMessage(message: Message): Promise { + await this.maybeUpdateContext([message]); + } + + private async maybeUpdateContext(newMessages: Message[] = []): Promise { + if (!this.contextManager || !this.bus) return; + + // Throttle updates to avoid excessive processing + const now = Date.now(); + if (now - this.lastUpdateTime < this.moduleConfig.updateThrottleMs!) { + return; + } + + try { + const result = await this.contextManager.updateContext(newMessages, this.factsBuffer); + + if (result.contextChanged) { + this.bus.emit('context.changed', result.context); + } + + this.lastUpdateTime = now; + } catch (err) { + console.error(`[ContextManagerModule] Context update failed:`, err); + this.status = 'degraded'; + this.bus?.emit('module.degraded', { + moduleId: this.id, + reason: String(err), + }); + } + } +} \ No newline at end of file diff --git a/packages/reference-implementation/src/context-manager.ts b/packages/reference-implementation/src/context-manager.ts new file mode 100644 index 0000000..8261746 --- /dev/null +++ b/packages/reference-implementation/src/context-manager.ts @@ -0,0 +1,258 @@ +/** + * Reference ContextManager implementation. + * + * The ContextManager handles attention and relevance filtering in the cognitive system. + * It maintains a sliding window of relevant context based on recency, importance, + * and semantic similarity. + * + * Key responsibilities: + * - Filter incoming perceptions by relevance + * - Maintain focused attention window + * - Emit context.changed events when focus shifts + * + * @see ROADMAP.md — "ContextManager implementation" + */ + +import type { Message, Fact, SearchResult } from '@ami/skeleton'; + +export interface ContextWindow { + /** Recent messages in the attention window */ + recentMessages: Message[]; + /** Relevant facts from semantic memory */ + relevantFacts: Fact[]; + /** Current attention topic/theme */ + focusTopic?: string; + /** Timestamp of last context update */ + lastUpdated: number; +} + +export interface RelevanceScore { + /** The message or fact being scored */ + item: Message | Fact; + /** Relevance score 0.0-1.0 */ + score: number; + /** Why this item is relevant */ + reasons: string[]; +} + +export interface ContextManagerConfig { + /** Maximum messages to keep in attention window */ + maxMessages?: number; + /** Maximum facts to keep in context */ + maxFacts?: number; + /** Minimum relevance score to include (0.0-1.0) */ + relevanceThreshold?: number; + /** Window of recency in milliseconds */ + recencyWindow?: number; +} + +export class ReferenceContextManager { + private readonly config: Required; + private currentContext: ContextWindow; + + constructor(config: ContextManagerConfig = {}) { + this.config = { + maxMessages: config.maxMessages ?? 20, + maxFacts: config.maxFacts ?? 10, + relevanceThreshold: config.relevanceThreshold ?? 0.3, + recencyWindow: config.recencyWindow ?? 300000, // 5 minutes + }; + + this.currentContext = { + recentMessages: [], + relevantFacts: [], + lastUpdated: Date.now(), + }; + } + + /** + * Update context with new messages and facts. + * Returns true if context changed significantly. + */ + async updateContext( + newMessages: Message[], + availableFacts?: Fact[] + ): Promise<{ contextChanged: boolean; context: ContextWindow }> { + const now = Date.now(); + const previousTopic = this.currentContext.focusTopic; + + // Filter messages by recency and relevance + const allMessages = [...this.currentContext.recentMessages, ...newMessages]; + const recentMessages = this.filterByRecency(allMessages, now); + const relevantMessages = this.filterByRelevance(recentMessages); + + // Extract current topic/focus from recent messages + const focusTopic = this.extractFocusTopic(relevantMessages); + + // Filter facts by relevance to current focus + let relevantFacts: Fact[] = []; + if (availableFacts && availableFacts.length > 0) { + relevantFacts = this.filterFactsByRelevance(availableFacts, focusTopic); + } else { + // Keep existing facts if no new ones provided + relevantFacts = this.currentContext.relevantFacts; + } + + // Update context + this.currentContext = { + recentMessages: relevantMessages.slice(-this.config.maxMessages), + relevantFacts: relevantFacts.slice(0, this.config.maxFacts), + focusTopic, + lastUpdated: now, + }; + + // Determine if context changed significantly + const contextChanged = + previousTopic !== focusTopic || + newMessages.length > 0 || + Boolean(availableFacts && availableFacts.length > 0); + + return { + contextChanged, + context: { ...this.currentContext } + }; + } + + /** + * Get the current context window. + */ + getCurrentContext(): ContextWindow { + return { ...this.currentContext }; + } + + /** + * Calculate relevance score for a message or fact. + */ + calculateRelevance(item: Message | Fact, focusTopic?: string): RelevanceScore { + const reasons: string[] = []; + let score = 0; + + // Base recency score + const age = Date.now() - item.timestamp; + const recencyScore = Math.max(0, 1 - age / this.config.recencyWindow); + if (recencyScore > 0.7) { + reasons.push('very recent'); + score += 0.3; + } else if (recencyScore > 0.3) { + reasons.push('recent'); + score += 0.2; + } + + // Content-based relevance + const content = 'content' in item ? item.content : item.text; + const contentScore = this.scoreContentRelevance(content, focusTopic); + score += contentScore.score; + reasons.push(...contentScore.reasons); + + // Message-specific scoring + if ('role' in item) { + // User messages are inherently important + if (item.role === 'user') { + score += 0.2; + reasons.push('user input'); + } + // Questions are important + if (content.includes('?')) { + score += 0.15; + reasons.push('question'); + } + } + + // Fact-specific scoring + if ('relations' in item) { + // Facts with more relations are more central + if (item.relations.length > 2) { + score += 0.1; + reasons.push('well-connected'); + } + } + + return { + item, + score: Math.min(1.0, score), + reasons + }; + } + + private filterByRecency(messages: Message[], now: number): Message[] { + return messages.filter(msg => + now - msg.timestamp <= this.config.recencyWindow + ); + } + + private filterByRelevance(messages: Message[]): Message[] { + const focusTopic = this.extractFocusTopic(messages); + + return messages + .map(msg => this.calculateRelevance(msg, focusTopic)) + .filter(scored => scored.score >= this.config.relevanceThreshold) + .sort((a, b) => b.score - a.score) + .map(scored => scored.item as Message); + } + + private filterFactsByRelevance(facts: Fact[], focusTopic?: string): Fact[] { + return facts + .map(fact => this.calculateRelevance(fact, focusTopic)) + .filter(scored => scored.score >= this.config.relevanceThreshold) + .sort((a, b) => b.score - a.score) + .map(scored => scored.item as Fact); + } + + private extractFocusTopic(messages: Message[]): string | undefined { + if (messages.length === 0) return undefined; + + // Simple topic extraction: look for repeated keywords in recent messages + const recentContent = messages + .slice(-5) // Last 5 messages + .map(msg => msg.content.toLowerCase()) + .join(' '); + + // Extract potential topics (simplified approach) + const words = recentContent + .split(/\s+/) + .filter(word => word.length > 3) + .filter(word => !/^(the|and|but|for|you|are|this|that|with|have|will|from)$/.test(word)); + + if (words.length === 0) return undefined; + + // Find most frequent meaningful word as topic + const wordCount = new Map(); + words.forEach(word => { + wordCount.set(word, (wordCount.get(word) || 0) + 1); + }); + + const sortedWords = Array.from(wordCount.entries()) + .sort((a, b) => b[1] - a[1]); + + return sortedWords[0]?.[0]; + } + + private scoreContentRelevance(content: string, focusTopic?: string): { score: number; reasons: string[] } { + const reasons: string[] = []; + let score = 0; + + // Topic relevance + if (focusTopic && content.toLowerCase().includes(focusTopic)) { + score += 0.4; + reasons.push(`mentions topic: ${focusTopic}`); + } + + // Keywords that indicate importance + const importantKeywords = ['error', 'bug', 'issue', 'problem', 'important', 'urgent', 'help']; + const hasImportantKeyword = importantKeywords.some(keyword => + content.toLowerCase().includes(keyword) + ); + if (hasImportantKeyword) { + score += 0.2; + reasons.push('contains important keyword'); + } + + // Length-based relevance (longer content often more substantive) + if (content.length > 100) { + score += 0.1; + reasons.push('substantive content'); + } + + return { score, reasons }; + } +} \ No newline at end of file diff --git a/packages/reference-implementation/src/index.ts b/packages/reference-implementation/src/index.ts index f04d91c..f65760f 100644 --- a/packages/reference-implementation/src/index.ts +++ b/packages/reference-implementation/src/index.ts @@ -9,5 +9,13 @@ export { ReferenceCognitiveBus } from './cognitive-bus.js'; export { ReferenceCognitiveRegistry } from './cognitive-registry.js'; export { DistillerModule } from './distiller-module.js'; export type { DistillerModuleConfig } from './distiller-module.js'; +export { ReferenceContextManager } from './context-manager.js'; +export type { + ContextWindow, + RelevanceScore, + ContextManagerConfig, +} from './context-manager.js'; +export { ContextManagerModule } from './context-manager-module.js'; +export type { ContextManagerModuleConfig } from './context-manager-module.js'; export { Ami } from './ami.js'; export type { AmiConfig } from './ami.js'; diff --git a/packages/skeleton/src/types.ts b/packages/skeleton/src/types.ts index 5dbe147..c9c2442 100644 --- a/packages/skeleton/src/types.ts +++ b/packages/skeleton/src/types.ts @@ -185,3 +185,20 @@ export interface CognitiveRegistry { export interface CognitiveLoop { step(): Promise; } + +// ─── Context Management ──────────────────────────────────────────────────── + +/** + * Context window maintained by the ContextManager. + * Represents the current attention focus and relevant information. + */ +export interface ContextWindow { + /** Recent messages in the attention window */ + recentMessages: Message[]; + /** Relevant facts from semantic memory */ + relevantFacts: Fact[]; + /** Current attention topic/theme */ + focusTopic?: string; + /** Timestamp of last context update */ + lastUpdated: number; +}