get_relevant_context
Retrieve the most relevant past memories for a user query or task, providing context to inform responses.
Instructions
Auto-retrieve the most relevant memories for a given user query or task. Use this at the start of any session or when the user references past context. Returns formatted context ready to inject into your reasoning.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| user_query | Yes | The current user query, task description, or topic to find context for |
Implementation Reference
- src/index.ts:73-89 (registration)Tool registration for 'get_relevant_context' in the ListToolsRequestSchema handler, defining name, description, and inputSchema requiring 'user_query'.
{ name: 'get_relevant_context', description: 'Auto-retrieve the most relevant memories for a given user query or task. ' + 'Use this at the start of any session or when the user references past context. ' + 'Returns formatted context ready to inject into your reasoning.', inputSchema: { type: 'object', properties: { user_query: { type: 'string', description: 'The current user query, task description, or topic to find context for', }, }, required: ['user_query'], }, }, - src/index.ts:206-235 (handler)Handler for 'get_relevant_context': accepts user_query, searches memories via BM25 (limit 6), touches results for recency tracking, groups by first tag, and returns formatted context.
case 'get_relevant_context': { const userQuery = String(a['user_query'] ?? '').trim(); if (!userQuery) return err('user_query is required'); const results = search(store.all(), userQuery, { limit: 6 }); if (results.length === 0) { return ok('No relevant memories found for this query. Memory is currently empty or no matches found.'); } results.forEach(r => store.touch(r.memory.key)); // Group by first tag for readability const groups = new Map<string, typeof results>(); results.forEach(r => { const group = r.memory.tags[0] ?? 'general'; if (!groups.has(group)) groups.set(group, []); groups.get(group)!.push(r); }); const sections: string[] = []; for (const [group, items] of groups) { const header = group.toUpperCase(); const bullets = items.map(r => `• ${r.memory.key}: ${r.memory.content}`).join('\n'); sections.push(`${header}:\n${bullets}`); } return ok( `Relevant context retrieved (${results.length} memories):\n\n${sections.join('\n\n')}` ); } - src/types.ts:1-17 (schema)Type definitions for Memory, SearchResult (used by the search function), MemoryFile, and MemoryStats.
export interface Memory { id: string; key: string; content: string; tags: string[]; importance: number; // 1-10 createdAt: string; updatedAt: string; accessCount: number; lastAccessed: string; } export interface SearchResult { memory: Memory; score: number; matchType: 'keyword' | 'exact'; } - src/search.ts:15-79 (helper)The BM25 search function used by get_relevant_context. Tokenizes query and documents, computes BM25 scores with importance weighting and recency decay, then returns sorted results.
export function search( memories: Memory[], query: string, opts: { limit?: number; tags?: string[] } = {} ): SearchResult[] { const { limit = 10, tags } = opts; const candidates = tags?.length ? memories.filter(m => tags.some(t => m.tags.includes(t))) : memories; if (candidates.length === 0) return []; const queryTerms = tokenize(query); if (queryTerms.length === 0) return []; // Pre-tokenize all documents const tokenized = candidates.map(m => ({ m, terms: tokenize(buildDocText(m)) })); // Compute document frequency for IDF const N = candidates.length; const df = new Map<string, number>(); queryTerms.forEach(qt => { const count = tokenized.filter(({ terms }) => terms.includes(qt)).length; df.set(qt, count); }); const avgLen = tokenized.reduce((a, { terms }) => a + terms.length, 0) / N; const k1 = 1.5; const b = 0.75; const scored = tokenized.map(({ m, terms }) => { const tf = new Map<string, number>(); terms.forEach(t => tf.set(t, (tf.get(t) ?? 0) + 1)); let bm25 = 0; for (const qt of queryTerms) { const freq = tf.get(qt) ?? 0; if (freq === 0) continue; const n = df.get(qt) ?? 0; const idf = Math.log((N - n + 0.5) / (n + 0.5) + 1); const tfNorm = (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * (terms.length / avgLen))); bm25 += idf * tfNorm; } // Exact key match boost const exactBoost = m.key.toLowerCase().includes(query.toLowerCase()) ? 2 : 0; // Tag exact match boost const tagBoost = m.tags.some(t => queryTerms.includes(t)) ? 1 : 0; // Importance weight: shifts score ±25% const importanceW = 1 + (m.importance - 5) * 0.05; // Gentle recency decay (half-life ~200 days) const daysSince = (Date.now() - new Date(m.updatedAt).getTime()) / 86_400_000; const recency = Math.exp(-daysSince * 0.003); const score = (bm25 + exactBoost + tagBoost) * importanceW * recency; const matchType: SearchResult['matchType'] = exactBoost > 0 ? 'exact' : 'keyword'; return { memory: m, score, matchType }; }); return scored .filter(r => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit); } - src/store.ts:80-87 (helper)The MemoryStore.touch() method used by get_relevant_context to increment access count and update lastAccessed timestamp when a memory is retrieved.
touch(key: string): void { const mem = this.data.memories.find(m => m.key === key); if (mem) { mem.accessCount++; mem.lastAccessed = new Date().toISOString(); this.persist(); } }