From 6986fb52d5e6fcdc4241e04acced2d9d7ac81922 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Wed, 24 Jun 2026 13:40:38 -0400 Subject: [PATCH 1/4] feat: add support for TS memory --- .../typescript/http/strands/base/main.ts | 48 +++++++++++++++++ .../typescript/http/strands/base/package.json | 4 +- .../strands/capabilities/memory/memory.ts | 54 +++++++++++++++++++ .../generate/__tests__/schema-mapper.test.ts | 14 ++++- .../agent/generate/schema-mapper.ts | 7 +-- .../__tests__/useGenerateWizard.test.tsx | 13 +++++ .../tui/screens/generate/useGenerateWizard.ts | 16 +++--- 7 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 src/assets/typescript/http/strands/capabilities/memory/memory.ts diff --git a/src/assets/typescript/http/strands/base/main.ts b/src/assets/typescript/http/strands/base/main.ts index 84428c181..4b33f583c 100644 --- a/src/assets/typescript/http/strands/base/main.ts +++ b/src/assets/typescript/http/strands/base/main.ts @@ -3,6 +3,9 @@ import { Agent, McpClient, tool, type ToolList } from '@strands-agents/sdk'; import { z } from 'zod'; import { loadModel } from './model/load.js'; import { getStreamableHttpMcpClient } from './mcp_client/client.js'; +{{#if hasMemory}} +import { getActorId, getOrCreateMemoryManager } from './memory/memory.js'; +{{/if}} // Define a collection of MCP clients (filter out anything that failed to initialize) const mcpClients: McpClient[] = [getStreamableHttpMcpClient()].filter( @@ -31,6 +34,25 @@ const SYSTEM_PROMPT = ` You are a helpful assistant. Use tools when appropriate. `; +{{#if hasMemory}} +const agentCache = new Map(); + +async function getOrCreateAgent(sessionId: string, actorId: string): Promise { + const key = `${actorId}:${sessionId}`; + let agent = agentCache.get(key); + if (agent) return agent; + + const model = await loadModel(); + agent = new Agent({ + model, + systemPrompt: SYSTEM_PROMPT, + tools, + memoryManager: getOrCreateMemoryManager(sessionId, actorId) ?? undefined, + }); + agentCache.set(key, agent); + return agent; +} +{{else}} let cachedAgent: Agent | null = null; async function getOrCreateAgent(): Promise { @@ -44,12 +66,37 @@ async function getOrCreateAgent(): Promise { } return cachedAgent; } +{{/if}} const app = new BedrockAgentCoreApp({ invocationHandler: { async *process(payload: any, context: any) { + {{#if hasMemory}} + const sessionId = context?.sessionId ?? 'default-session'; + const actorId = getActorId(payload, context); + const agent = await getOrCreateAgent(sessionId, actorId); + {{else}} const agent = await getOrCreateAgent(); + {{/if}} + {{#if hasMemory}} + try { + for await (const event of agent.stream(payload.prompt ?? '')) { + if ( + event.type === 'modelStreamUpdateEvent' && + event.event?.type === 'modelContentBlockDeltaEvent' && + event.event.delta?.type === 'textDelta' + ) { + yield { data: event.event.delta.text }; + } + } + } finally { + // Drain in-flight createEvent calls before the runtime can reclaim + // the session microVM. flush() is the durability mechanism — without + // it, an idle reclamation can lose the tail of the conversation. + await agent.memoryManager?.flush(); + } + {{else}} for await (const event of agent.stream(payload.prompt ?? '')) { if ( event.type === 'modelStreamUpdateEvent' && @@ -59,6 +106,7 @@ const app = new BedrockAgentCoreApp({ yield { data: event.event.delta.text }; } } + {{/if}} }, }, }); diff --git a/src/assets/typescript/http/strands/base/package.json b/src/assets/typescript/http/strands/base/package.json index 09680ec6f..ab0c447f3 100644 --- a/src/assets/typescript/http/strands/base/package.json +++ b/src/assets/typescript/http/strands/base/package.json @@ -20,8 +20,8 @@ "@google/genai": "^1.40.0", {{/if}} "@modelcontextprotocol/sdk": "^1.25.2", - "@strands-agents/sdk": "1.0.0-rc.4", - "bedrock-agentcore": "^0.2.4", + "@strands-agents/sdk": "^1.5.0", + "bedrock-agentcore": "^0.3.0", "tsx": "^4.19.0", "zod": "^4.4.3" }, diff --git a/src/assets/typescript/http/strands/capabilities/memory/memory.ts b/src/assets/typescript/http/strands/capabilities/memory/memory.ts new file mode 100644 index 000000000..fbccef1f7 --- /dev/null +++ b/src/assets/typescript/http/strands/capabilities/memory/memory.ts @@ -0,0 +1,54 @@ +import { MemoryManager } from '@strands-agents/sdk'; +import { createAgentCoreMemoryStores } from 'bedrock-agentcore/experimental/memory/strands'; + +const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}}; + +const CUSTOM_ACTOR_ID_HEADER = 'x-amzn-bedrock-agentcore-runtime-custom-actor-id'; + +export function getActorId(payload: any, context: any): string { + // Multi-actor agents: caller passes user via the custom header (declared in + // requestHeaderAllowlist) or as a `userId` field in the invocation payload. + // Single-actor agents: actorId == sessionId is fine. + return ( + context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ?? + payload?.userId ?? + context?.sessionId + ); +} + +const memoryManagerCache = new Map(); + +export function getOrCreateMemoryManager(sessionId: string, actorId: string): MemoryManager | null { + if (!MEMORY_ID) return null; + + const key = `${actorId}:${sessionId}`; + let manager = memoryManagerCache.get(key); + if (manager) return manager; + + const stores = createAgentCoreMemoryStores({ + memoryId: MEMORY_ID, + actorId, + sessionId, + namespaces: [ +{{#if (includes memoryProviders.[0].strategies "SEMANTIC")}} + { namespace: '/users/{actorId}/facts' }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}} + { namespace: '/users/{actorId}/preferences' }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "EPISODIC")}} + { namespace: '/episodes/{actorId}/{sessionId}' }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} + { namespace: '/summaries/{actorId}/{sessionId}' }, +{{/if}} + ], + // readMode defaults to 'per-namespace' (one retrieve call per namespace). + // Switch to 'subtree' to consolidate to a single hierarchical recall call. + extraction: true, + }); + + manager = new MemoryManager({ stores }); + memoryManagerCache.set(key, manager); + return manager; +} diff --git a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts index a96db26a9..43c4755f7 100644 --- a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts +++ b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts @@ -580,11 +580,23 @@ describe('mapGenerateConfigToRenderConfig - needsOs', () => { expect(result.s3Mounts).toEqual([{ mountPath: '/mnt/s3' }]); }); - it('TypeScript agents have hasMemory=false and empty memoryProviders', async () => { + it('TypeScript Strands agents populate memoryProviders for longAndShortTerm', async () => { const result = await mapGenerateConfigToRenderConfig( { ...base, language: 'TypeScript', memory: 'longAndShortTerm' as const }, [] ); + expect(result.hasMemory).toBe(true); + expect(result.memoryProviders).toHaveLength(1); + expect(result.memoryProviders[0]!.name).toBe('RenderAgentMemory'); + expect(result.memoryProviders[0]!.envVarName).toBe('MEMORY_RENDERAGENTMEMORY_ID'); + expect(result.memoryProviders[0]!.strategies).toEqual(['SEMANTIC', 'USER_PREFERENCE', 'SUMMARIZATION', 'EPISODIC']); + }); + + it('TypeScript Strands agents have hasMemory=false when memory is "none"', async () => { + const result = await mapGenerateConfigToRenderConfig( + { ...base, language: 'TypeScript', memory: 'none' as const }, + [] + ); expect(result.hasMemory).toBe(false); expect(result.memoryProviders).toEqual([]); }); diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index e9584e248..61f7ae65a 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -276,7 +276,7 @@ export async function mapGenerateConfigToRenderConfig( sdkFramework: config.sdk, targetLanguage: config.language, modelProvider: config.modelProvider, - hasMemory: isMcp || config.language === 'TypeScript' ? false : config.memory !== 'none', + hasMemory: isMcp ? false : config.memory !== 'none', hasIdentity: isMcp ? false : identityProviders.length > 0, hasGateway: gatewayProviders.length > 0, hasPayment: await (async () => { @@ -289,10 +289,7 @@ export async function mapGenerateConfigToRenderConfig( })(), isVpc: config.networkMode === 'VPC', buildType: config.buildType, - memoryProviders: - isMcp || config.language === 'TypeScript' - ? [] - : mapMemoryOptionToMemoryProviders(config.memory, config.projectName), + memoryProviders: isMcp ? [] : mapMemoryOptionToMemoryProviders(config.memory, config.projectName), identityProviders: isMcp ? [] : identityProviders, gatewayProviders, gatewayAuthTypes: [...new Set(gatewayProviders.map(g => g.authType))], diff --git a/src/cli/tui/screens/generate/__tests__/useGenerateWizard.test.tsx b/src/cli/tui/screens/generate/__tests__/useGenerateWizard.test.tsx index a800af0ff..c42e459b7 100644 --- a/src/cli/tui/screens/generate/__tests__/useGenerateWizard.test.tsx +++ b/src/cli/tui/screens/generate/__tests__/useGenerateWizard.test.tsx @@ -75,6 +75,19 @@ describe('useGenerateWizard — advanced config gate', () => { const frame = lastFrame()!; expect(frame).toMatch(/memory,advanced/); }); + + it('Strands SDK inserts memory before advanced for TypeScript', () => { + const { ref, lastFrame } = setup(); + act(() => { + ref.current!.wizard.setProjectName('Test'); + ref.current!.wizard.setLanguage('TypeScript'); + ref.current!.wizard.setBuildType('CodeZip'); + ref.current!.wizard.setProtocol('HTTP'); + ref.current!.wizard.setSdk('Strands'); + }); + const frame = lastFrame()!; + expect(frame).toMatch(/memory,advanced/); + }); }); describe('setAdvanced routing', () => { diff --git a/src/cli/tui/screens/generate/useGenerateWizard.ts b/src/cli/tui/screens/generate/useGenerateWizard.ts index e4267fc7c..b94ebc76e 100644 --- a/src/cli/tui/screens/generate/useGenerateWizard.ts +++ b/src/cli/tui/screens/generate/useGenerateWizard.ts @@ -54,7 +54,7 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { if (config.modelProvider === 'Bedrock') { filtered = filtered.filter(s => s !== 'apiKey'); } - if (sdkSelected && config.sdk === 'Strands' && config.language !== 'TypeScript') { + if (sdkSelected && config.sdk === 'Strands') { const advancedIndex = filtered.indexOf('advanced'); filtered = [...filtered.slice(0, advancedIndex), 'memory', ...filtered.slice(advancedIndex)]; } @@ -136,7 +136,7 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { }, []); const setLanguage = useCallback((language: GenerateConfig['language']) => { - setConfig(c => ({ ...c, language, memory: language === 'TypeScript' ? 'none' : c.memory })); + setConfig(c => ({ ...c, language })); setStep('buildType'); }, []); @@ -174,34 +174,34 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { // Non-Bedrock providers need API key step if (modelProvider !== 'Bedrock') { setStep('apiKey'); - } else if (config.sdk === 'Strands' && config.language !== 'TypeScript') { + } else if (config.sdk === 'Strands') { setStep('memory'); } else { setStep('advanced'); } }, - [config.sdk, config.language] + [config.sdk] ); const setApiKey = useCallback( (apiKey: string | undefined) => { setConfig(c => ({ ...c, apiKey })); - if (config.sdk === 'Strands' && config.language !== 'TypeScript') { + if (config.sdk === 'Strands') { setStep('memory'); } else { setStep('advanced'); } }, - [config.sdk, config.language] + [config.sdk] ); const skipApiKey = useCallback(() => { - if (config.sdk === 'Strands' && config.language !== 'TypeScript') { + if (config.sdk === 'Strands') { setStep('memory'); } else { setStep('advanced'); } - }, [config.sdk, config.language]); + }, [config.sdk]); const setMemory = useCallback((memory: MemoryOption) => { setConfig(c => ({ ...c, memory })); From 08b4403a80186ef2ac9a83101c45de170dab9a8d Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 25 Jun 2026 09:23:53 -0400 Subject: [PATCH 2/4] fix: update snapshots and minor bug fixes --- .../assets.snapshot.test.ts.snap | 116 +++++++++++++++++- .../strands/capabilities/memory/memory.ts | 5 +- .../generate/__tests__/schema-mapper.test.ts | 9 ++ .../agent/generate/schema-mapper.ts | 8 +- 4 files changed, 132 insertions(+), 6 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 3e611c48f..b1f998864 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -851,6 +851,7 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f "typescript/http/strands/base/model/load.ts", "typescript/http/strands/base/package.json", "typescript/http/strands/base/tsconfig.json", + "typescript/http/strands/capabilities/memory/memory.ts", "typescript/http/vercelai/base/README.md", "typescript/http/vercelai/base/gitignore.template", "typescript/http/vercelai/base/main.ts", @@ -7537,6 +7538,9 @@ import { Agent, McpClient, tool, type ToolList } from '@strands-agents/sdk'; import { z } from 'zod'; import { loadModel } from './model/load.js'; import { getStreamableHttpMcpClient } from './mcp_client/client.js'; +{{#if hasMemory}} +import { getActorId, getOrCreateMemoryManager } from './memory/memory.js'; +{{/if}} // Define a collection of MCP clients (filter out anything that failed to initialize) const mcpClients: McpClient[] = [getStreamableHttpMcpClient()].filter( @@ -7565,6 +7569,25 @@ const SYSTEM_PROMPT = \` You are a helpful assistant. Use tools when appropriate. \`; +{{#if hasMemory}} +const agentCache = new Map(); + +async function getOrCreateAgent(sessionId: string, actorId: string): Promise { + const key = \`\${actorId}:\${sessionId}\`; + let agent = agentCache.get(key); + if (agent) return agent; + + const model = await loadModel(); + agent = new Agent({ + model, + systemPrompt: SYSTEM_PROMPT, + tools, + memoryManager: getOrCreateMemoryManager(sessionId, actorId) ?? undefined, + }); + agentCache.set(key, agent); + return agent; +} +{{else}} let cachedAgent: Agent | null = null; async function getOrCreateAgent(): Promise { @@ -7578,12 +7601,37 @@ async function getOrCreateAgent(): Promise { } return cachedAgent; } +{{/if}} const app = new BedrockAgentCoreApp({ invocationHandler: { async *process(payload: any, context: any) { + {{#if hasMemory}} + const sessionId = context?.sessionId ?? 'default-session'; + const actorId = getActorId(payload, context); + const agent = await getOrCreateAgent(sessionId, actorId); + {{else}} const agent = await getOrCreateAgent(); - + {{/if}} + + {{#if hasMemory}} + try { + for await (const event of agent.stream(payload.prompt ?? '')) { + if ( + event.type === 'modelStreamUpdateEvent' && + event.event?.type === 'modelContentBlockDeltaEvent' && + event.event.delta?.type === 'textDelta' + ) { + yield { data: event.event.delta.text }; + } + } + } finally { + // Drain in-flight createEvent calls before the runtime can reclaim + // the session microVM. flush() is the durability mechanism — without + // it, an idle reclamation can lose the tail of the conversation. + await agent.memoryManager?.flush(); + } + {{else}} for await (const event of agent.stream(payload.prompt ?? '')) { if ( event.type === 'modelStreamUpdateEvent' && @@ -7593,6 +7641,7 @@ const app = new BedrockAgentCoreApp({ yield { data: event.event.delta.text }; } } + {{/if}} }, }, }); @@ -7745,8 +7794,8 @@ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/ "@google/genai": "^1.40.0", {{/if}} "@modelcontextprotocol/sdk": "^1.25.2", - "@strands-agents/sdk": "1.0.0-rc.4", - "bedrock-agentcore": "^0.2.4", + "@strands-agents/sdk": "^1.5.0", + "bedrock-agentcore": "^0.3.0", "tsx": "^4.19.0", "zod": "^4.4.3" }, @@ -7786,6 +7835,67 @@ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/ " `; +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/capabilities/memory/memory.ts should match snapshot 1`] = ` +"import { randomUUID } from 'node:crypto'; +import { MemoryManager } from '@strands-agents/sdk'; +import { createAgentCoreMemoryStores } from 'bedrock-agentcore/experimental/memory/strands'; + +const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}}; + +const CUSTOM_ACTOR_ID_HEADER = 'x-amzn-bedrock-agentcore-runtime-custom-actor-id'; + +export function getActorId(payload: any, context: any): string { + // Multi-actor agents: caller passes user via the custom header (declared in + // requestHeaderAllowlist) or as a \`userId\` field in the invocation payload. + // Single-actor agents: actorId == sessionId is fine. + // RandomUUID fallback when all sources are absent + return ( + context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ?? + payload?.userId ?? + context?.sessionId ?? + randomUUID() + ); +} + +const memoryManagerCache = new Map(); + +export function getOrCreateMemoryManager(sessionId: string, actorId: string): MemoryManager | null { + if (!MEMORY_ID) return null; + + const key = \`\${actorId}:\${sessionId}\`; + let manager = memoryManagerCache.get(key); + if (manager) return manager; + + const stores = createAgentCoreMemoryStores({ + memoryId: MEMORY_ID, + actorId, + sessionId, + namespaces: [ +{{#if (includes memoryProviders.[0].strategies "SEMANTIC")}} + { namespace: '/users/{actorId}/facts' }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}} + { namespace: '/users/{actorId}/preferences' }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "EPISODIC")}} + { namespace: '/episodes/{actorId}/{sessionId}' }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} + { namespace: '/summaries/{actorId}/{sessionId}' }, +{{/if}} + ], + // readMode defaults to 'per-namespace' (one retrieve call per namespace). + // Switch to 'subtree' to consolidate to a single hierarchical recall call. + extraction: true, + }); + + manager = new MemoryManager({ stores }); + memoryManagerCache.set(key, manager); + return manager; +} +" +`; + exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/README.md should match snapshot 1`] = ` "This is a project generated by the AgentCore CLI! diff --git a/src/assets/typescript/http/strands/capabilities/memory/memory.ts b/src/assets/typescript/http/strands/capabilities/memory/memory.ts index fbccef1f7..9bb7e849a 100644 --- a/src/assets/typescript/http/strands/capabilities/memory/memory.ts +++ b/src/assets/typescript/http/strands/capabilities/memory/memory.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto'; import { MemoryManager } from '@strands-agents/sdk'; import { createAgentCoreMemoryStores } from 'bedrock-agentcore/experimental/memory/strands'; @@ -9,10 +10,12 @@ export function getActorId(payload: any, context: any): string { // Multi-actor agents: caller passes user via the custom header (declared in // requestHeaderAllowlist) or as a `userId` field in the invocation payload. // Single-actor agents: actorId == sessionId is fine. + // RandomUUID fallback when all sources are absent return ( context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ?? payload?.userId ?? - context?.sessionId + context?.sessionId ?? + randomUUID() ); } diff --git a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts index 43c4755f7..9f1e2f8a9 100644 --- a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts +++ b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts @@ -600,4 +600,13 @@ describe('mapGenerateConfigToRenderConfig - needsOs', () => { expect(result.hasMemory).toBe(false); expect(result.memoryProviders).toEqual([]); }); + + it('TypeScript non-Strands agents have hasMemory=false even with memory selected', async () => { + const result = await mapGenerateConfigToRenderConfig( + { ...base, language: 'TypeScript', sdk: 'VercelAI', memory: 'longAndShortTerm' as const }, + [] + ); + expect(result.hasMemory).toBe(false); + expect(result.memoryProviders).toEqual([]); + }); }); diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index 61f7ae65a..f5f6b916a 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -276,7 +276,8 @@ export async function mapGenerateConfigToRenderConfig( sdkFramework: config.sdk, targetLanguage: config.language, modelProvider: config.modelProvider, - hasMemory: isMcp ? false : config.memory !== 'none', + hasMemory: + isMcp || (config.language === 'TypeScript' && config.sdk !== 'Strands') ? false : config.memory !== 'none', hasIdentity: isMcp ? false : identityProviders.length > 0, hasGateway: gatewayProviders.length > 0, hasPayment: await (async () => { @@ -289,7 +290,10 @@ export async function mapGenerateConfigToRenderConfig( })(), isVpc: config.networkMode === 'VPC', buildType: config.buildType, - memoryProviders: isMcp ? [] : mapMemoryOptionToMemoryProviders(config.memory, config.projectName), + memoryProviders: + isMcp || (config.language === 'TypeScript' && config.sdk !== 'Strands') + ? [] + : mapMemoryOptionToMemoryProviders(config.memory, config.projectName), identityProviders: isMcp ? [] : identityProviders, gatewayProviders, gatewayAuthTypes: [...new Set(gatewayProviders.map(g => g.authType))], From 0b0f763ca41eea28ff9a233e9debf939622ee491 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 25 Jun 2026 15:00:37 -0400 Subject: [PATCH 3/4] fix: hide STM for TS strands path --- src/assets/typescript/http/strands/base/package.json | 1 + .../agent/generate/__tests__/schema-mapper.test.ts | 8 ++++++++ src/cli/operations/agent/generate/schema-mapper.ts | 6 +++++- src/cli/tui/screens/generate/GenerateWizardUI.tsx | 7 +++++-- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/assets/typescript/http/strands/base/package.json b/src/assets/typescript/http/strands/base/package.json index ab0c447f3..4e390deea 100644 --- a/src/assets/typescript/http/strands/base/package.json +++ b/src/assets/typescript/http/strands/base/package.json @@ -20,6 +20,7 @@ "@google/genai": "^1.40.0", {{/if}} "@modelcontextprotocol/sdk": "^1.25.2", + "@opentelemetry/api": "^1.9.0", "@strands-agents/sdk": "^1.5.0", "bedrock-agentcore": "^0.3.0", "tsx": "^4.19.0", diff --git a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts index 9f1e2f8a9..579377afe 100644 --- a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts +++ b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts @@ -609,4 +609,12 @@ describe('mapGenerateConfigToRenderConfig - needsOs', () => { expect(result.hasMemory).toBe(false); expect(result.memoryProviders).toEqual([]); }); + + it('TypeScript Strands agents have hasMemory=false for shortTerm only', async () => { + const result = await mapGenerateConfigToRenderConfig( + { ...base, language: 'TypeScript', memory: 'shortTerm' as const }, + [] + ); + expect(result.hasMemory).toBe(false); + }); }); diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index f5f6b916a..17b06884f 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -277,7 +277,11 @@ export async function mapGenerateConfigToRenderConfig( targetLanguage: config.language, modelProvider: config.modelProvider, hasMemory: - isMcp || (config.language === 'TypeScript' && config.sdk !== 'Strands') ? false : config.memory !== 'none', + isMcp || (config.language === 'TypeScript' && config.sdk !== 'Strands') + ? false + : config.language === 'TypeScript' + ? config.memory === 'longAndShortTerm' + : config.memory !== 'none', hasIdentity: isMcp ? false : identityProviders.length > 0, hasGateway: gatewayProviders.length > 0, hasPayment: await (async () => { diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index aef8f914b..5c7217a37 100644 --- a/src/cli/tui/screens/generate/GenerateWizardUI.tsx +++ b/src/cli/tui/screens/generate/GenerateWizardUI.tsx @@ -93,8 +93,11 @@ export function GenerateWizardUI({ title: o.title, description: o.description, })); - case 'memory': - return MEMORY_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); + case 'memory': { + const isTypescriptStrands = wizard.config.language === 'TypeScript' && wizard.config.sdk === 'Strands'; + const options = isTypescriptStrands ? MEMORY_OPTIONS.filter(o => o.id !== 'shortTerm') : MEMORY_OPTIONS; + return options.map(o => ({ id: o.id, title: o.title, description: o.description })); + } case 'networkMode': return NETWORK_MODE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); case 'authorizerType': From 3da448b5d3cdb586b2d7923188b5a28a9a78d93b Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 25 Jun 2026 15:13:14 -0400 Subject: [PATCH 4/4] fix: append actor id header when valid memory strategy selected --- .../__snapshots__/assets.snapshot.test.ts.snap | 16 ++++++---------- .../http/strands/capabilities/memory/memory.ts | 15 +++++---------- .../operations/agent/generate/schema-mapper.ts | 13 +++++++++++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index b1f998864..b8740989a 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -7794,6 +7794,7 @@ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/ "@google/genai": "^1.40.0", {{/if}} "@modelcontextprotocol/sdk": "^1.25.2", + "@opentelemetry/api": "^1.9.0", "@strands-agents/sdk": "^1.5.0", "bedrock-agentcore": "^0.3.0", "tsx": "^4.19.0", @@ -7845,16 +7846,11 @@ const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}}; const CUSTOM_ACTOR_ID_HEADER = 'x-amzn-bedrock-agentcore-runtime-custom-actor-id'; export function getActorId(payload: any, context: any): string { - // Multi-actor agents: caller passes user via the custom header (declared in - // requestHeaderAllowlist) or as a \`userId\` field in the invocation payload. - // Single-actor agents: actorId == sessionId is fine. - // RandomUUID fallback when all sources are absent - return ( - context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ?? - payload?.userId ?? - context?.sessionId ?? - randomUUID() - ); + const raw = + context?.headers?.[CUSTOM_ACTOR_ID_HEADER] || + payload?.userId || + context?.sessionId; + return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : randomUUID(); } const memoryManagerCache = new Map(); diff --git a/src/assets/typescript/http/strands/capabilities/memory/memory.ts b/src/assets/typescript/http/strands/capabilities/memory/memory.ts index 9bb7e849a..546b7c6bd 100644 --- a/src/assets/typescript/http/strands/capabilities/memory/memory.ts +++ b/src/assets/typescript/http/strands/capabilities/memory/memory.ts @@ -7,16 +7,11 @@ const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}}; const CUSTOM_ACTOR_ID_HEADER = 'x-amzn-bedrock-agentcore-runtime-custom-actor-id'; export function getActorId(payload: any, context: any): string { - // Multi-actor agents: caller passes user via the custom header (declared in - // requestHeaderAllowlist) or as a `userId` field in the invocation payload. - // Single-actor agents: actorId == sessionId is fine. - // RandomUUID fallback when all sources are absent - return ( - context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ?? - payload?.userId ?? - context?.sessionId ?? - randomUUID() - ); + const raw = + context?.headers?.[CUSTOM_ACTOR_ID_HEADER] || + payload?.userId || + context?.sessionId; + return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : randomUUID(); } const memoryManagerCache = new Map(); diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index 17b06884f..6a3f68810 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -113,11 +113,20 @@ export function mapModelProviderToCredentials(modelProvider: ModelProvider, proj /** * Maps GenerateConfig to v2 AgentEnvSpec resource. */ +const ACTOR_ID_HEADER = 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id'; + export function mapGenerateConfigToAgent(config: GenerateConfig): AgentEnvSpec { const codeLocation = `${APP_DIR}/${config.projectName}/`; const protocol = config.protocol ?? 'HTTP'; const networkMode = config.networkMode ?? DEFAULT_NETWORK_MODE; + const needsActorHeader = + config.language === 'TypeScript' && config.sdk === 'Strands' && config.memory === 'longAndShortTerm'; + const headerAllowlist = [ + ...(config.requestHeaderAllowlist ?? []), + ...(needsActorHeader && !(config.requestHeaderAllowlist ?? []).includes(ACTOR_ID_HEADER) ? [ACTOR_ID_HEADER] : []), + ]; + return { name: config.projectName, build: config.buildType ?? 'CodeZip', @@ -137,8 +146,8 @@ export function mapGenerateConfigToAgent(config: GenerateConfig): AgentEnvSpec { securityGroups: config.securityGroups, }, }), - ...(config.requestHeaderAllowlist?.length && { - requestHeaderAllowlist: config.requestHeaderAllowlist, + ...(headerAllowlist.length > 0 && { + requestHeaderAllowlist: headerAllowlist, }), ...(config.authorizerType && { authorizerType: config.authorizerType }), ...(config.authorizerType === 'CUSTOM_JWT' &&