Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 109 additions & 3 deletions src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -7565,6 +7569,25 @@ const SYSTEM_PROMPT = \`
You are a helpful assistant. Use tools when appropriate.
\`;

{{#if hasMemory}}
const agentCache = new Map<string, Agent>();

async function getOrCreateAgent(sessionId: string, actorId: string): Promise<Agent> {
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<Agent> {
Expand All @@ -7578,12 +7601,37 @@ async function getOrCreateAgent(): Promise<Agent> {
}
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' &&
Expand All @@ -7593,6 +7641,7 @@ const app = new BedrockAgentCoreApp({
yield { data: event.event.delta.text };
}
}
{{/if}}
},
},
});
Expand Down Expand Up @@ -7745,8 +7794,9 @@ 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",
"@opentelemetry/api": "^1.9.0",
"@strands-agents/sdk": "^1.5.0",
"bedrock-agentcore": "^0.3.0",
"tsx": "^4.19.0",
"zod": "^4.4.3"
},
Expand Down Expand Up @@ -7786,6 +7836,62 @@ 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 {
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<string, MemoryManager>();

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!

Expand Down
48 changes: 48 additions & 0 deletions src/assets/typescript/http/strands/base/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -31,6 +34,25 @@ const SYSTEM_PROMPT = `
You are a helpful assistant. Use tools when appropriate.
`;

{{#if hasMemory}}
const agentCache = new Map<string, Agent>();

async function getOrCreateAgent(sessionId: string, actorId: string): Promise<Agent> {
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<Agent> {
Expand All @@ -44,12 +66,37 @@ async function getOrCreateAgent(): Promise<Agent> {
}
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' &&
Expand All @@ -59,6 +106,7 @@ const app = new BedrockAgentCoreApp({
yield { data: event.event.delta.text };
}
}
{{/if}}
},
},
});
Expand Down
5 changes: 3 additions & 2 deletions src/assets/typescript/http/strands/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
"@google/genai": "^1.40.0",
{{/if}}
"@modelcontextprotocol/sdk": "^1.25.2",
"@strands-agents/sdk": "1.0.0-rc.4",
"bedrock-agentcore": "^0.2.4",
"@opentelemetry/api": "^1.9.0",
"@strands-agents/sdk": "^1.5.0",
"bedrock-agentcore": "^0.3.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bedrock-agentcore@^0.3.0 is not on npm yet (latest published is 0.2.4), and the import bedrock-agentcore/experimental/memory/strands used in memory.ts ships in that unreleased version. Until 0.3.x is published, every newly generated TS Strands project (memory or not) will fail npm install. Per the PR description this PR shouldn't merge until the SDK is released — when it is, please also pin exact versions (drop the carets on both bedrock-agentcore and @strands-agents/sdk) per the note in the PR description.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be updated in PR once new SDK version is released.

"tsx": "^4.19.0",
"zod": "^4.4.3"
},
Expand Down
52 changes: 52 additions & 0 deletions src/assets/typescript/http/strands/capabilities/memory/memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 {
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<string, MemoryManager>();

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -580,12 +580,41 @@ 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([]);
});

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([]);
});

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);
});
});
Loading
Loading