diff --git a/docs/memory.md b/docs/memory.md index 0f38513d2..6e1a0f63f 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -92,7 +92,7 @@ If you created an Strands agent without memory and want to integrate it with you retrieval_config = { f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5) + f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5) } return AgentCoreMemorySessionManager( @@ -153,11 +153,11 @@ async def invoke(payload, context): The `create` and `add agent` commands accept a `--memory` flag with one of three shorthand values. Each maps to a specific memory configuration: -| Shorthand | Strategies Created | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `none` | No memory resource created | -| `shortTerm` | Memory with no strategies (session context via event expiry only, default 30 days) | -| `longAndShortTerm` | Memory with four strategies: `SEMANTIC` (`/users/{actorId}/facts`), `USER_PREFERENCE` (`/users/{actorId}/preferences`), `SUMMARIZATION` (`/summaries/{actorId}/{sessionId}`), `EPISODIC` (`/episodes/{actorId}/{sessionId}`, reflection: `/episodes/{actorId}`) | +| Shorthand | Strategies Created | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `none` | No memory resource created | +| `shortTerm` | Memory with no strategies (session context via event expiry only, default 30 days) | +| `longAndShortTerm` | Memory with four strategies: `SEMANTIC` (`/users/{actorId}/facts`), `USER_PREFERENCE` (`/users/{actorId}/preferences`), `SUMMARIZATION` (`/summaries/{actorId}`), `EPISODIC` (`/episodes/{actorId}/{sessionId}`, reflection: `/episodes/{actorId}`) | **Short-term memory** provides basic conversation context within a session — events are stored and expire after the configured duration, but no long-term extraction or search is performed. diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 6cfcc62d2..548365c5f 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -2255,7 +2255,7 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), {{/if}} {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} - f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), {{/if}} } {{/if}} @@ -3277,7 +3277,7 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), {{/if}} {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} - f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), {{/if}} } {{/if}} @@ -6545,7 +6545,7 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), {{/if}} {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} - f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), {{/if}} } {{/if}} diff --git a/src/assets/__tests__/summarization-namespace.test.ts b/src/assets/__tests__/summarization-namespace.test.ts new file mode 100644 index 000000000..cbf61edc0 --- /dev/null +++ b/src/assets/__tests__/summarization-namespace.test.ts @@ -0,0 +1,43 @@ +/** + * Regression test for cross-session SUMMARIZATION recall (issue #665). + * + * The SUMMARIZATION retrieval namespace must be actor-scoped (`/summaries/{actor_id}`), + * not session-scoped. The SDK's namespace_path is a hierarchical prefix match, so a + * per-session prefix (`/summaries/{actor_id}/{session_id}`) only ever matches the current + * session's own summaries and never surfaces summaries written by prior sessions — + * silently breaking cross-session recall. This guards against another silent revert + * (see PR #1299, reverted by squash release #1547). + */ +import Handlebars from 'handlebars'; +import * as fs from 'fs'; +import * as path from 'path'; +import { describe, expect, it } from 'vitest'; +// Importing render registers the `includes` Handlebars helper used by the template. +import '../../cli/templates/render.js'; + +const FLAVORS = ['http', 'agui', 'a2a'] as const; + +function renderSessionTemplate(flavor: string): string { + const templatePath = path.resolve( + __dirname, + '..', + 'python', + flavor, + 'strands', + 'capabilities', + 'memory', + 'session.py' + ); + const content = fs.readFileSync(templatePath, 'utf-8'); + return Handlebars.compile(content)({ + memoryProviders: [{ envVarName: 'MEMORY_TEST_ID', strategies: ['SUMMARIZATION'] }], + }); +} + +describe('SUMMARIZATION retrieval namespace', () => { + it.each(FLAVORS)('%s session.py uses an actor-scoped summary namespace', flavor => { + const rendered = renderSessionTemplate(flavor); + expect(rendered).toContain('f"/summaries/{actor_id}": RetrievalConfig'); + expect(rendered).not.toContain('/summaries/{actor_id}/{session_id}'); + }); +}); diff --git a/src/assets/python/a2a/strands/capabilities/memory/session.py b/src/assets/python/a2a/strands/capabilities/memory/session.py index 1a8b7e5a3..f029d5d1a 100644 --- a/src/assets/python/a2a/strands/capabilities/memory/session.py +++ b/src/assets/python/a2a/strands/capabilities/memory/session.py @@ -28,7 +28,7 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), {{/if}} {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} - f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), {{/if}} } {{/if}} diff --git a/src/assets/python/agui/strands/capabilities/memory/session.py b/src/assets/python/agui/strands/capabilities/memory/session.py index 1a8b7e5a3..f029d5d1a 100644 --- a/src/assets/python/agui/strands/capabilities/memory/session.py +++ b/src/assets/python/agui/strands/capabilities/memory/session.py @@ -28,7 +28,7 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), {{/if}} {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} - f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), {{/if}} } {{/if}} diff --git a/src/assets/python/http/strands/capabilities/memory/session.py b/src/assets/python/http/strands/capabilities/memory/session.py index a00b46666..e9dc85e86 100644 --- a/src/assets/python/http/strands/capabilities/memory/session.py +++ b/src/assets/python/http/strands/capabilities/memory/session.py @@ -28,7 +28,7 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), {{/if}} {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} - f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), {{/if}} } {{/if}}