diff --git a/src/assets/templates/a2a-python-strands/README.md b/src/assets/templates/a2a-python-strands/README.md index 7da24eb69a..feef497dc6 100644 --- a/src/assets/templates/a2a-python-strands/README.md +++ b/src/assets/templates/a2a-python-strands/README.md @@ -8,6 +8,13 @@ This agent speaks the A2A protocol. Other agents discover it via its agent card `/.well-known/agent-card.json` and invoke it with JSON-RPC (`message/send`, `message/stream`) at the server root, matching the AgentCore Runtime A2A service contract. +## Memory + +The template provisions semantic and episodic memory. `memory/session.py` retrieves up to three +semantic facts for the actor and five episodes for the current session. The default +`relevance_score=0.0` selects results by `top_k` without score filtering. To opt into score +filtering, increase `relevance_score` in the retrieval configuration. + ## Adding Tools Define tools with the `@tool` decorator in `main.py` and add them to the `tools` list: diff --git a/src/assets/templates/a2a-python-strands/memory/session.py b/src/assets/templates/a2a-python-strands/memory/session.py index 6372a2f685..cd4c56aa82 100644 --- a/src/assets/templates/a2a-python-strands/memory/session.py +++ b/src/assets/templates/a2a-python-strands/memory/session.py @@ -18,10 +18,8 @@ def get_memory_session_manager( session_id = session_id or uuid.uuid4().hex retrieval_config = { - f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), - f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.0), + f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.0), } return AgentCoreMemorySessionManager( diff --git a/src/assets/templates/agent-python-strands/README.md b/src/assets/templates/agent-python-strands/README.md index 0e60591533..214088ffcb 100644 --- a/src/assets/templates/agent-python-strands/README.md +++ b/src/assets/templates/agent-python-strands/README.md @@ -13,6 +13,13 @@ file defines a Starlette ASGI app with the Strands Agent SDK running within. `model/load.py` instantiates the chosen model provider. +## Memory + +The template provisions semantic and episodic memory. `memory/session.py` retrieves up to three +semantic facts for the actor and five episodes for the current session. The default +`relevance_score=0.0` selects results by `top_k` without score filtering. To opt into score +filtering, increase `relevance_score` in the retrieval configuration. + ## Input Validation Validate invocation input before forwarding it to Strands. Keep plain prompts typed as strings. If the app accepts a diff --git a/src/assets/templates/agent-python-strands/memory/session.py b/src/assets/templates/agent-python-strands/memory/session.py index aa8e46e5a0..baa569eb90 100644 --- a/src/assets/templates/agent-python-strands/memory/session.py +++ b/src/assets/templates/agent-python-strands/memory/session.py @@ -14,10 +14,8 @@ def get_memory_session_manager( return None retrieval_config = { - f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), - f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.0), + f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.0), } return AgentCoreMemorySessionManager( diff --git a/src/assets/templates/agent-typescript-strands/README.md b/src/assets/templates/agent-typescript-strands/README.md index edeaed4ca3..cda7ede527 100644 --- a/src/assets/templates/agent-typescript-strands/README.md +++ b/src/assets/templates/agent-typescript-strands/README.md @@ -17,6 +17,11 @@ from `.env.local` under `agentcore dev`; the `openai_compatible` provider is the pointed at the base URL you gave the `agentcore create` wizard, baked into `load.ts` as `clientConfig.baseURL`. +## Memory + +The template provisions semantic and episodic memory. `memory/memory.ts` retrieves semantic +facts for the actor and episodes for the current session, with score filtering disabled by default. + ## Input Validation The generated Zod request schema keeps the prompt and actor ID typed as strings before forwarding them to Strands. diff --git a/src/assets/templates/agent-typescript-strands/memory/memory.ts b/src/assets/templates/agent-typescript-strands/memory/memory.ts index b5252932ac..abb1060a27 100644 --- a/src/assets/templates/agent-typescript-strands/memory/memory.ts +++ b/src/assets/templates/agent-typescript-strands/memory/memory.ts @@ -18,9 +18,7 @@ export function getOrCreateMemoryManager(sessionId: string, actorId: string): Me sessionId, namespaces: [ { namespace: '/users/{actorId}/facts' }, - { namespace: '/users/{actorId}/preferences' }, { namespace: '/episodes/{actorId}/{sessionId}' }, - { namespace: '/summaries/{actorId}' }, ], // readMode defaults to 'per-namespace' (one retrieve call per namespace). // Switch to 'subtree' to consolidate to a single hierarchical recall call. diff --git a/src/assets/templates/agui-python-strands/README.md b/src/assets/templates/agui-python-strands/README.md index b1662331f5..5a0918afe0 100644 --- a/src/assets/templates/agui-python-strands/README.md +++ b/src/assets/templates/agui-python-strands/README.md @@ -9,6 +9,13 @@ streams agent-to-UI events over HTTP. It exposes an `/invocations` endpoint that an AG-UI `RunAgentInput` body and streams the response back as AG-UI server-sent events, matching the AgentCore Runtime HTTP service contract. +## Memory + +The template provisions semantic and episodic memory. `memory/session.py` retrieves up to three +semantic facts for the actor and five episodes for the current session. The default +`relevance_score=0.0` selects results by `top_k` without score filtering. To opt into score +filtering, increase `relevance_score` in the retrieval configuration. + ## Adding Tools Define tools with the `@tool` decorator in `main.py` and add them to the agent's `tools` list: diff --git a/src/assets/templates/agui-python-strands/memory/session.py b/src/assets/templates/agui-python-strands/memory/session.py index 6372a2f685..cd4c56aa82 100644 --- a/src/assets/templates/agui-python-strands/memory/session.py +++ b/src/assets/templates/agui-python-strands/memory/session.py @@ -18,10 +18,8 @@ def get_memory_session_manager( session_id = session_id or uuid.uuid4().hex retrieval_config = { - f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), - f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.0), + f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.0), } return AgentCoreMemorySessionManager( diff --git a/src/assets/templates/export-harness-python/memory/session.py b/src/assets/templates/export-harness-python/memory/session.py index 38bcf49f93..e9764a3fcd 100644 --- a/src/assets/templates/export-harness-python/memory/session.py +++ b/src/assets/templates/export-harness-python/memory/session.py @@ -20,16 +20,16 @@ def get_memory_session_manager( {{#if memoryStrategies.length}} retrieval_config = { {{#if (includes memoryStrategies "SEMANTIC")}} - f"/users/{actor_id}/facts": RetrievalConfig(top_k={{#if memoryRetrievalTopK}}{{memoryRetrievalTopK}}{{else}}3{{/if}}, relevance_score={{#if memoryRetrievalRelevanceScore}}{{memoryRetrievalRelevanceScore}}{{else}}0.5{{/if}}), + f"/users/{actor_id}/facts": RetrievalConfig(top_k={{#if memoryRetrievalTopK}}{{memoryRetrievalTopK}}{{else}}3{{/if}}, relevance_score={{#if memoryRetrievalRelevanceScore}}{{memoryRetrievalRelevanceScore}}{{else}}0.0{{/if}}), {{/if}} {{#if (includes memoryStrategies "USER_PREFERENCE")}} - f"/users/{actor_id}/preferences": RetrievalConfig(top_k={{#if memoryRetrievalTopK}}{{memoryRetrievalTopK}}{{else}}3{{/if}}, relevance_score={{#if memoryRetrievalRelevanceScore}}{{memoryRetrievalRelevanceScore}}{{else}}0.5{{/if}}), + f"/users/{actor_id}/preferences": RetrievalConfig(top_k={{#if memoryRetrievalTopK}}{{memoryRetrievalTopK}}{{else}}3{{/if}}, relevance_score={{#if memoryRetrievalRelevanceScore}}{{memoryRetrievalRelevanceScore}}{{else}}0.0{{/if}}), {{/if}} {{#if (includes memoryStrategies "EPISODIC")}} - f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k={{#if memoryRetrievalTopK}}{{memoryRetrievalTopK}}{{else}}5{{/if}}, relevance_score={{#if memoryRetrievalRelevanceScore}}{{memoryRetrievalRelevanceScore}}{{else}}0.5{{/if}}), + f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k={{#if memoryRetrievalTopK}}{{memoryRetrievalTopK}}{{else}}5{{/if}}, relevance_score={{#if memoryRetrievalRelevanceScore}}{{memoryRetrievalRelevanceScore}}{{else}}0.0{{/if}}), {{/if}} {{#if (includes memoryStrategies "SUMMARIZATION")}} - f"/summaries/{actor_id}": RetrievalConfig(top_k={{#if memoryRetrievalTopK}}{{memoryRetrievalTopK}}{{else}}3{{/if}}, relevance_score={{#if memoryRetrievalRelevanceScore}}{{memoryRetrievalRelevanceScore}}{{else}}0.5{{/if}}), + f"/summaries/{actor_id}": RetrievalConfig(top_k={{#if memoryRetrievalTopK}}{{memoryRetrievalTopK}}{{else}}3{{/if}}, relevance_score={{#if memoryRetrievalRelevanceScore}}{{memoryRetrievalRelevanceScore}}{{else}}0.0{{/if}}), {{/if}} } {{/if}} diff --git a/src/core/project/__snapshots__/manager.test.ts.snap b/src/core/project/__snapshots__/manager.test.ts.snap index 9af00244b0..e33bd9075f 100644 --- a/src/core/project/__snapshots__/manager.test.ts.snap +++ b/src/core/project/__snapshots__/manager.test.ts.snap @@ -54,18 +54,6 @@ exports[`FsProjectManager.create snapshots the Strands project manifest and runt ], "type": "SEMANTIC", }, - { - "namespaceTemplates": [ - "/users/{actorId}/preferences", - ], - "type": "USER_PREFERENCE", - }, - { - "namespaceTemplates": [ - "/summaries/{actorId}/{sessionId}", - ], - "type": "SUMMARIZATION", - }, { "namespaceTemplates": [ "/episodes/{actorId}/{sessionId}", @@ -125,18 +113,6 @@ exports[`FsProjectManager.create snapshots the Strands TypeScript project manife ], "type": "SEMANTIC", }, - { - "namespaceTemplates": [ - "/users/{actorId}/preferences", - ], - "type": "USER_PREFERENCE", - }, - { - "namespaceTemplates": [ - "/summaries/{actorId}/{sessionId}", - ], - "type": "SUMMARIZATION", - }, { "namespaceTemplates": [ "/episodes/{actorId}/{sessionId}", @@ -197,18 +173,6 @@ exports[`FsProjectManager.create snapshots the Strands A2A project manifest and ], "type": "SEMANTIC", }, - { - "namespaceTemplates": [ - "/users/{actorId}/preferences", - ], - "type": "USER_PREFERENCE", - }, - { - "namespaceTemplates": [ - "/summaries/{actorId}/{sessionId}", - ], - "type": "SUMMARIZATION", - }, { "namespaceTemplates": [ "/episodes/{actorId}/{sessionId}", diff --git a/src/core/project/bedrockAgentImport/strandsTranslator.ts b/src/core/project/bedrockAgentImport/strandsTranslator.ts index d9acdc0fd3..abd3631c4f 100644 --- a/src/core/project/bedrockAgentImport/strandsTranslator.ts +++ b/src/core/project/bedrockAgentImport/strandsTranslator.ts @@ -248,10 +248,8 @@ function generateStrandsMemoryModule(request: BedrockAgentImportRequest): string const retrievalConfig = request.memory === "longAndShortTerm" ? ` retrieval_config = { - f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5), - f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), - f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.0), + f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.0), } ` : " retrieval_config = {}\n"; diff --git a/src/core/project/bedrockAgentImport/translator.test.ts b/src/core/project/bedrockAgentImport/translator.test.ts index a73894dd93..ac6175d9b8 100644 --- a/src/core/project/bedrockAgentImport/translator.test.ts +++ b/src/core/project/bedrockAgentImport/translator.test.ts @@ -93,6 +93,22 @@ describe("StrandsBedrockAgentTranslator", () => { expect(plan.files["IMPORT_NOTES.md"]).toContain("Strands guardrail"); }); + test("retrieves semantic and episodic memory without a default score filter", () => { + const memory = new StrandsBedrockAgentTranslator(snapshot(), request).translate().files[ + "memory.py" + ]!; + + expect(memory).toContain( + 'f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.0)', + ); + expect(memory).toContain( + 'f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.0)', + ); + expect(memory.match(/RetrievalConfig\(/g)).toHaveLength(2); + expect(memory).not.toContain("/preferences"); + expect(memory).not.toContain("/summaries/"); + }); + test("pins a code-interpreter dependency that can coexist with strands-agents 1.x", () => { const pyproject = new StrandsBedrockAgentTranslator(snapshot(), request).translate().files[ "pyproject.toml" diff --git a/src/core/project/manager.export.test.ts b/src/core/project/manager.export.test.ts index 0c36621ba8..37995ae2c4 100644 --- a/src/core/project/manager.export.test.ts +++ b/src/core/project/manager.export.test.ts @@ -179,7 +179,14 @@ describe("FsProjectManager.exportHarness rendered tree", () => { resourceConfig: { name: "chat_history", eventExpiryDuration: 30, - strategies: [{ type: "SEMANTIC" }], + strategies: [ + { type: "SEMANTIC" }, + { + type: "EPISODIC", + namespaceTemplates: ["/episodes/{actorId}/{sessionId}"], + reflectionNamespaceTemplates: ["/episodes/{actorId}"], + }, + ], }, }), ); @@ -188,41 +195,52 @@ describe("FsProjectManager.exportHarness rendered tree", () => { const session = await Bun.file(join(result.agentPath, "memory", "session.py")).text(); expect(session).toContain('MEMORY_ID = os.getenv("MEMORY_CHAT_HISTORY_ID")'); + expect(session).toContain( + 'f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.0)', + ); + expect(session).toContain( + 'f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.0)', + ); + expect(session).not.toContain("/preferences"); + expect(session).not.toContain("/summaries/"); expect(await Bun.file(join(result.agentPath, "main.py")).text()).toContain( "from memory.session import get_memory_session_manager", ); expect(result.notes).toEqual([]); }); - test("renders memory retrieval tuning and notes messagesCount", async () => { - const { manager: subject } = manager(); - let project = await projectWithHarness(subject, { - memory: { - mode: "existing", - name: "chat_history", - messagesCount: 12, - retrievalConfig: { topK: 8, relevanceScore: 0.7 }, - }, - }); - project = await drain( - subject.addResource(project, { - resourceType: "memory", - resourceConfig: { + test.each([0, 0.7])( + "renders relevanceScore=%p and notes messagesCount", + async (relevanceScore) => { + const { manager: subject } = manager(); + let project = await projectWithHarness(subject, { + memory: { + mode: "existing", name: "chat_history", - eventExpiryDuration: 30, - strategies: [{ type: "SEMANTIC" }], + messagesCount: 12, + retrievalConfig: { topK: 8, relevanceScore }, }, - }), - ); + }); + project = await drain( + subject.addResource(project, { + resourceType: "memory", + resourceConfig: { + name: "chat_history", + eventExpiryDuration: 30, + strategies: [{ type: "SEMANTIC" }], + }, + }), + ); - const result = await drain(subject.exportHarness(project, exportInput())); + const result = await drain(subject.exportHarness(project, exportInput())); - const session = await Bun.file(join(result.agentPath, "memory", "session.py")).text(); - expect(session).toContain("RetrievalConfig(top_k=8, relevance_score=0.7)"); - expect(result.notes.map((note) => note.category)).toContain( - "Memory messagesCount is not directly portable to Strands", - ); - }); + const session = await Bun.file(join(result.agentPath, "memory", "session.py")).text(); + expect(session).toContain(`RetrievalConfig(top_k=8, relevance_score=${relevanceScore})`); + expect(result.notes.map((note) => note.category)).toContain( + "Memory messagesCount is not directly portable to Strands", + ); + }, + ); test("renders OpenAI Responses settings with compatible Strands extras", async () => { const { manager: subject } = manager(); diff --git a/src/core/project/templates/runtime.test.ts b/src/core/project/templates/runtime.test.ts index c131bbcfbf..cd5ca140a4 100644 --- a/src/core/project/templates/runtime.test.ts +++ b/src/core/project/templates/runtime.test.ts @@ -120,17 +120,18 @@ describe("runtime templates", () => { }); test.each<[RuntimeTemplateShortcutName, string[]]>([ - ["agent-python-strands", ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], - ["a2a-python-strands", ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], - ["agent-typescript-strands", ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], + ["agent-python-strands", ["SEMANTIC", "EPISODIC"]], + ["agent-python-strands-container", ["SEMANTIC", "EPISODIC"]], + ["a2a-python-strands", ["SEMANTIC", "EPISODIC"]], + ["agent-typescript-strands", ["SEMANTIC", "EPISODIC"]], ["agent-python-minimal", []], ["agent-python-langchain", []], ["agent-typescript-vercel", []], ["mcp-python-fastmcp", []], - ["agui-python-strands", ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], + ["agui-python-strands", ["SEMANTIC", "EPISODIC"]], ["environment-python-bma", []], ])("%s ships with its pre-configured memory", async (template, expectedStrategies) => { - const { spec } = await addTemplate("my_agent", template); + const { spec, appDir } = await addTemplate("my_agent", template); const memory = (spec.memories ?? []).find( (candidate: { name: string }) => candidate.name === "my_agentMemory", ); @@ -140,6 +141,34 @@ describe("runtime templates", () => { } expect(memory).toMatchObject({ name: "my_agentMemory", eventExpiryDuration: 30 }); expect(memory.strategies.map(({ type }: { type: string }) => type)).toEqual(expectedStrategies); + expect(memory.strategies).toEqual([ + { type: "SEMANTIC", namespaceTemplates: ["/users/{actorId}/facts"] }, + { + type: "EPISODIC", + namespaceTemplates: ["/episodes/{actorId}/{sessionId}"], + reflectionNamespaceTemplates: ["/episodes/{actorId}"], + }, + ]); + + const isTypeScript = template === "agent-typescript-strands"; + const memoryCode = await Bun.file( + join(appDir, "memory", isTypeScript ? "memory.ts" : "session.py"), + ).text(); + expect(memoryCode).not.toContain("/preferences"); + expect(memoryCode).not.toContain("/summaries/"); + if (isTypeScript) { + expect(memoryCode).toContain("{ namespace: '/users/{actorId}/facts' }"); + expect(memoryCode).toContain("{ namespace: '/episodes/{actorId}/{sessionId}' }"); + expect(memoryCode.match(/namespace:/g)).toHaveLength(2); + } else { + expect(memoryCode).toContain( + 'f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.0)', + ); + expect(memoryCode).toContain( + 'f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.0)', + ); + expect(memoryCode.match(/RetrievalConfig\(/g)).toHaveLength(2); + } }); test.each<[RuntimeTemplateShortcutName, string]>([ diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index c67ef6f2a7..f46787bc2f 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -420,8 +420,6 @@ describe("project add runtime --agent-id (Bedrock Agent import)", () => { expect(spec.memories).toMatchObject([{ name: "support_proxyMemory" }]); expect(spec.memories[0].strategies.map(({ type }: { type: string }) => type)).toEqual([ "SEMANTIC", - "USER_PREFERENCE", - "SUMMARIZATION", "EPISODIC", ]); }); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 41ebbde2e4..0ace4da996 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -211,8 +211,6 @@ describe("project create", () => { expect(memory).toMatchObject({ name: "agentMemory", eventExpiryDuration: 30 }); expect(memory.strategies.map(({ type }: { type: string }) => type)).toEqual([ "SEMANTIC", - "USER_PREFERENCE", - "SUMMARIZATION", "EPISODIC", ]); expect(await Bun.file(join(projectRoot, "app", "agent", "main.py")).exists()).toBe(true); diff --git a/src/handlers/project/shortcuts.ts b/src/handlers/project/shortcuts.ts index 3d16e3c887..9fb198b07a 100644 --- a/src/handlers/project/shortcuts.ts +++ b/src/handlers/project/shortcuts.ts @@ -15,15 +15,13 @@ export function getDefaultMemorySpec(runtimeName: string): Memory { return { name: defaultMemoryName(runtimeName), eventExpiryDuration: 30, - strategies: (["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"] as const).map( - (type) => ({ - type, - namespaceTemplates: DEFAULT_STRATEGY_NAMESPACE_TEMPLATES[type], - ...(type === "EPISODIC" && { - reflectionNamespaceTemplates: DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, - }), + strategies: (["SEMANTIC", "EPISODIC"] as const).map((type) => ({ + type, + namespaceTemplates: DEFAULT_STRATEGY_NAMESPACE_TEMPLATES[type], + ...(type === "EPISODIC" && { + reflectionNamespaceTemplates: DEFAULT_EPISODIC_REFLECTION_NAMESPACE_TEMPLATES, }), - ), + })), }; }