Skip to content
Draft
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
36 changes: 36 additions & 0 deletions src/assets/templates/strands-http-typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a project generated by the AgentCore CLI!

# Layout

The generated application code lives at the agent root directory. At the root, there is a `.gitignore` file, an
`agentcore/` folder which represents the configurations and state associated with this project. Other `agentcore`
commands like `deploy`, `dev`, and `invoke` rely on the configuration stored here.

## Agent Root

The main entrypoint to your app is defined in `main.ts`. Using the AgentCore SDK `BedrockAgentCoreApp`, this file
defines an HTTP server that streams tokens from your chosen Agent framework SDK.

`model/load.ts` instantiates your chosen model provider.

## Input Validation

The generated Zod request schema keeps plain prompts typed as strings before forwarding them to Strands. Retain this
validation when extending the request shape, and pass only prompt text to the agent.

## Environment Variables

| Variable | Required | Description |
| --- | --- | --- |
{{#if hasIdentity}}| `{{identityProviders.[0].envVarName}}` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) |
{{/if}}| `LOCAL_DEV` | No | Set to `1` to use `.env.local` instead of AgentCore Identity |

# Developing locally

If installation was successful, `node_modules/` is already populated with dependencies.

`agentcore project dev` will start a local server using `tsx watch main.ts` for hot reload on 0.0.0.0:8080.

# Deployment

After providing credentials, `agentcore project deploy` will deploy your project into Amazon Bedrock AgentCore.
22 changes: 22 additions & 0 deletions src/assets/templates/strands-http-typescript/gitignore.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Environment variables
.env
.env.*

# Node
node_modules/
dist/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db
149 changes: 149 additions & 0 deletions src/assets/templates/strands-http-typescript/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
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(
(client): client is McpClient => Boolean(client)
);

// Define a collection of tools used by the model
const tools: ToolList = [];

// Define a simple function tool — the Zod schema gives us type inference and runtime validation for free
const addNumbers = tool({
name: 'add_numbers',
description: 'Return the sum of two numbers',
inputSchema: z.object({
a: z.number(),
b: z.number(),
}),
callback: async ({ a, b }) => a + b,
});
tools.push(addNumbers);

// Add MCP clients to tools
tools.push(...mcpClients);

const SYSTEM_PROMPT = `
You are a helpful assistant. Use tools when appropriate.
`;

const requestSchema = z.object({
prompt: z.string().default(''),
});

{{#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}}
const AGENT_CACHE_LIMIT = 128;

// Reuses one Agent per sessionId so each session keeps its own in-process
// conversation history (best-effort; resets on cold start). A Map preserves
// insertion order, so it doubles as an LRU bounded to 128 sessions — a local
// dev process serving many sessions cannot leak history between them or grow
// without bound. On AgentCore Runtime each microVM serves a single session, so
// this holds one entry. For durable history, attach memory.
const agentCache = new Map<string, Agent>();

async function getOrCreateAgent(sessionId: string): Promise<Agent> {
const existing = agentCache.get(sessionId);
if (existing) {
agentCache.delete(sessionId);
agentCache.set(sessionId, existing);
return existing;
}
if (agentCache.size >= AGENT_CACHE_LIMIT) {
const oldest = agentCache.keys().next().value;
if (oldest !== undefined) agentCache.delete(oldest);
}
const model = await loadModel();
const agent = new Agent({
model,
systemPrompt: SYSTEM_PROMPT,
tools,
});
agentCache.set(sessionId, agent);
return agent;
}
{{/if}}

const app = new BedrockAgentCoreApp({
invocationHandler: {
requestSchema,
async *process(payload, context) {
{{#if hasMemory}}
const sessionId = context?.sessionId ?? 'default-session';
const actorId = getActorId(payload, context);
const agent = await getOrCreateAgent(sessionId, actorId);
{{else}}
const sessionId = context?.sessionId ?? 'default-session';
const agent = await getOrCreateAgent(sessionId);
{{/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}}
// Snapshot history before streaming so a failed turn can be rolled back.
// Agent.stream() appends the user message before invoking the model; on a
// mid-stream error that user turn would otherwise linger in the cached
// agent, and the next turn for this session would send consecutive user
// messages (rejected by providers that require strict role alternation,
// e.g. Anthropic). Restoring on error keeps the session reusable.
const snapshot = agent.takeSnapshot({ include: ['messages'] });
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 };
}
}
} catch (error) {
agent.loadSnapshot(snapshot);
throw error;
}
{{/if}}
},
},
});

app.run({ port: parseInt(process.env.PORT ?? '8080') });
11 changes: 11 additions & 0 deletions src/assets/templates/strands-http-typescript/mcp_client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { McpClient } from '@strands-agents/sdk';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

// ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
const EXAMPLE_MCP_ENDPOINT = 'https://mcp.exa.ai/mcp';

export function getStreamableHttpMcpClient(): McpClient {
// to use an MCP server that supports bearer authentication, add a headers() callback to requestInit
const transport = new StreamableHTTPClientTransport(new URL(EXAMPLE_MCP_ENDPOINT));
return new McpClient({ transport });
}
52 changes: 52 additions & 0 deletions src/assets/templates/strands-http-typescript/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.{{memoryEnvVarName}};

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 memoryStrategies "SEMANTIC")}}
{ namespace: '/users/{actorId}/facts' },
{{/if}}
{{#if (includes memoryStrategies "USER_PREFERENCE")}}
{ namespace: '/users/{actorId}/preferences' },
{{/if}}
{{#if (includes memoryStrategies "EPISODIC")}}
{ namespace: '/episodes/{actorId}/{sessionId}' },
{{/if}}
{{#if (includes memoryStrategies "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;
}
102 changes: 102 additions & 0 deletions src/assets/templates/strands-http-typescript/model/load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{{#if (eq modelProvider "Bedrock")}}
import { BedrockModel } from '@strands-agents/sdk/models/bedrock';

export function loadModel(): BedrockModel {
return new BedrockModel({ modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0' });
}
{{/if}}
{{#if (eq modelProvider "Anthropic")}}
import { AnthropicModel } from '@strands-agents/sdk/models/anthropic';
import { withApiKey } from 'bedrock-agentcore/identity';

const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';

async function getApiKey(): Promise<string> {
if (process.env.LOCAL_DEV === '1') {
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
throw new Error(`${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local`);
}
return apiKey;
}
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
}

let _model: AnthropicModel | undefined;

export async function loadModel(): Promise<AnthropicModel> {
if (!_model) {
const apiKey = await getApiKey();
_model = new AnthropicModel({
apiKey,
modelId: 'claude-sonnet-4-5-20250929',
maxTokens: 5000,
});
}
return _model;
}
{{/if}}
{{#if (eq modelProvider "OpenAI")}}
import { OpenAIModel } from '@strands-agents/sdk/models/openai';
import { withApiKey } from 'bedrock-agentcore/identity';

const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';

async function getApiKey(): Promise<string> {
if (process.env.LOCAL_DEV === '1') {
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error(`${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local`);
}
return apiKey;
}
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
}

let _model: OpenAIModel | undefined;

export async function loadModel(): Promise<OpenAIModel> {
if (!_model) {
const apiKey = await getApiKey();
_model = new OpenAIModel({
api: 'chat',
apiKey,
modelId: 'gpt-4.1',
});
}
return _model;
}
{{/if}}
{{#if (eq modelProvider "Gemini")}}
import { GoogleModel } from '@strands-agents/sdk/models/google';
import { withApiKey } from 'bedrock-agentcore/identity';

const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';

async function getApiKey(): Promise<string> {
if (process.env.LOCAL_DEV === '1') {
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error(`${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local`);
}
return apiKey;
}
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
}

let _model: GoogleModel | undefined;

export async function loadModel(): Promise<GoogleModel> {
if (!_model) {
const apiKey = await getApiKey();
_model = new GoogleModel({
apiKey,
modelId: 'gemini-2.5-flash',
});
}
return _model;
}
{{/if}}
Loading
Loading