Skip to content

Commit d06172c

Browse files
committed
feat(memory): add default MemoryHydeRetriever, auto-attach when LLM available
Memory-specific HyDE retriever generates hypothetical stored traces for improved recall on vague queries. Auto-attached in CognitiveMemoryManager when any LLM invoker is configured. Remains opt-in per query via retrieve({ hyde: true }). Based on the generation effect from cognitive psychology.
1 parent 12930fd commit d06172c

3 files changed

Lines changed: 170 additions & 0 deletions

File tree

src/memory/CognitiveMemoryManager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,19 @@ export class CognitiveMemoryManager implements ICognitiveMemoryManager {
342342
}
343343
}
344344

345+
// --- HyDE Retriever (auto-attached when any LLM invoker is available) ---
346+
// Generates hypothetical memory traces for improved recall on vague queries.
347+
// Opt-in per query via retrieve({ hyde: true }). Based on the "generation
348+
// effect" — generating what a memory WOULD look like activates retrieval
349+
// pathways more effectively than raw query embedding.
350+
const anyLlmInvoker = config.reflector?.llmInvoker
351+
?? config.observer?.llmInvoker
352+
?? config.featureDetectionLlmInvoker;
353+
if (anyLlmInvoker && !this.hydeRetriever) {
354+
const { MemoryHydeRetriever } = await import('./retrieval/hyde/MemoryHydeRetriever.js');
355+
this.hydeRetriever = new MemoryHydeRetriever(anyLlmInvoker) as unknown as HydeRetriever;
356+
}
357+
345358
this.initialized = true;
346359
}
347360

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* @fileoverview Memory-specific HyDE (Hypothetical Document Embedding) retriever.
3+
*
4+
* Improves memory recall for vague or abstract queries by generating a
5+
* hypothetical memory trace BEFORE embedding. The hypothesis is closer
6+
* in embedding space to actual stored traces than the raw query.
7+
*
8+
* Cognitive science grounding: this mirrors the "generation effect" —
9+
* generating information about a topic activates related neural pathways
10+
* more strongly than passive recognition. By generating what a memory
11+
* WOULD look like, we activate the right retrieval pathways.
12+
*
13+
* Effective for:
14+
* - Abstract queries ("that deployment discussion")
15+
* - Emotional recall ("when they were upset")
16+
* - Temporal queries ("something from last week")
17+
* - Vague references ("the thing about cats")
18+
*
19+
* Auto-attached by CognitiveMemoryManager when any LLM invoker is available.
20+
* Remains opt-in per query via `options.hyde: true` on `retrieve()`.
21+
*
22+
* @module agentos/memory/retrieval/hyde/MemoryHydeRetriever
23+
* @see {@link CognitiveMemoryManager.retrieve} — consumes the hypothesis
24+
*/
25+
26+
/** LLM invoker function signature matching AgentOS observer/reflector convention. */
27+
type LlmInvoker = (systemPrompt: string, userPrompt: string) => Promise<string>;
28+
29+
/**
30+
* System prompt for hypothesis generation.
31+
*
32+
* Instructs the LLM to output what a STORED memory trace would look like,
33+
* not to answer the query. This produces embeddings that are semantically
34+
* closer to actual stored traces than raw recall queries.
35+
*/
36+
const HYDE_SYSTEM_PROMPT = `You are a memory system. Given a recall query, generate what a stored memory trace about this topic would look like. Write it as a first-person observation note, 1-2 sentences, as if you recorded it when it happened.
37+
38+
Do NOT answer the query. Generate what the STORED MEMORY would say.
39+
40+
Examples:
41+
Query: "what does the user do for work?"
42+
Hypothesis: "User mentioned they are a software engineer working on backend systems at a startup."
43+
44+
Query: "when were they upset?"
45+
Hypothesis: "User expressed frustration and stress about a missed deadline. Emotional tone was tense."
46+
47+
Query: "that thing about cats"
48+
Hypothesis: "User talked about having two cats named Luna and Mochi. They seem important to the user."`;
49+
50+
/**
51+
* Memory-specific HyDE retriever that generates hypothetical memory traces.
52+
*
53+
* Implements the same `generateHypothesis()` interface expected by
54+
* CognitiveMemoryManager so it can be assigned via `setHydeRetriever()`.
55+
*
56+
* Lightweight: uses `maxTokens: 150` with no chain-of-thought. Target
57+
* latency is under 500ms with a fast model.
58+
*
59+
* @example
60+
* ```ts
61+
* const retriever = new MemoryHydeRetriever(llmInvoker);
62+
* const result = await retriever.generateHypothesis('what does the user like?');
63+
* // result.hypothesis = "User mentioned they enjoy hiking and cooking..."
64+
* ```
65+
*/
66+
export class MemoryHydeRetriever {
67+
private readonly llmInvoker: LlmInvoker;
68+
69+
/**
70+
* @param llmInvoker - Function that calls an LLM with (systemPrompt, userPrompt).
71+
* Typically reused from the observer, reflector, or feature detection config.
72+
*/
73+
constructor(llmInvoker: LlmInvoker) {
74+
this.llmInvoker = llmInvoker;
75+
}
76+
77+
/**
78+
* Generate a hypothetical memory trace for a recall query.
79+
*
80+
* The generated hypothesis is used as the embedding input for vector
81+
* search, producing results that are more semantically aligned with
82+
* actual stored traces.
83+
*
84+
* Returns the same shape as `HydeRetriever.generateHypothesis()` so
85+
* CognitiveMemoryManager can use it interchangeably.
86+
*
87+
* @param query - The recall query (e.g., "what does the user do for work?")
88+
* @returns Object with `hypothesis` text and `latencyMs` timing
89+
*/
90+
async generateHypothesis(query: string): Promise<{ hypothesis: string; latencyMs: number }> {
91+
const start = Date.now();
92+
try {
93+
const hypothesis = await this.llmInvoker(HYDE_SYSTEM_PROMPT, `Query: "${query}"`);
94+
return {
95+
hypothesis: hypothesis.trim(),
96+
latencyMs: Date.now() - start,
97+
};
98+
} catch {
99+
// HyDE is non-critical — return empty hypothesis to fall through to raw query
100+
return { hypothesis: '', latencyMs: Date.now() - start };
101+
}
102+
}
103+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { MemoryHydeRetriever } from '../MemoryHydeRetriever.js';
3+
4+
describe('MemoryHydeRetriever', () => {
5+
it('generates a hypothetical memory trace for a recall query', async () => {
6+
const llmInvoker = vi.fn().mockResolvedValue(
7+
'User mentioned they are a software engineer working on backend systems.'
8+
);
9+
10+
const retriever = new MemoryHydeRetriever(llmInvoker);
11+
const result = await retriever.generateHypothesis('what does the user do for work?');
12+
13+
expect(result.hypothesis).toBe(
14+
'User mentioned they are a software engineer working on backend systems.'
15+
);
16+
expect(result.latencyMs).toBeGreaterThanOrEqual(0);
17+
expect(llmInvoker).toHaveBeenCalledTimes(1);
18+
19+
// System prompt should instruct to generate a stored memory, not answer the query
20+
const systemPrompt = llmInvoker.mock.calls[0][0] as string;
21+
expect(systemPrompt).toContain('STORED MEMORY');
22+
expect(systemPrompt).toContain('Do NOT answer the query');
23+
});
24+
25+
it('returns empty hypothesis when LLM fails', async () => {
26+
const llmInvoker = vi.fn().mockRejectedValue(new Error('LLM unavailable'));
27+
const retriever = new MemoryHydeRetriever(llmInvoker);
28+
const result = await retriever.generateHypothesis('test query');
29+
30+
expect(result.hypothesis).toBe('');
31+
expect(result.latencyMs).toBeGreaterThanOrEqual(0);
32+
});
33+
34+
it('trims whitespace from hypothesis', async () => {
35+
const llmInvoker = vi.fn().mockResolvedValue(
36+
' User likes hiking and cooking. \n'
37+
);
38+
39+
const retriever = new MemoryHydeRetriever(llmInvoker);
40+
const result = await retriever.generateHypothesis('what does the user like?');
41+
42+
expect(result.hypothesis).toBe('User likes hiking and cooking.');
43+
});
44+
45+
it('passes the query in the user prompt', async () => {
46+
const llmInvoker = vi.fn().mockResolvedValue('hypothesis');
47+
const retriever = new MemoryHydeRetriever(llmInvoker);
48+
49+
await retriever.generateHypothesis('tell me about their family');
50+
51+
const userPrompt = llmInvoker.mock.calls[0][1] as string;
52+
expect(userPrompt).toContain('tell me about their family');
53+
});
54+
});

0 commit comments

Comments
 (0)