AgentCore: step by step
A hands-on walkthrough — build the agent mock-first, then swap one adapter at a time (Bedrock, AgentCore Memory, Identity, Gateway, Observability), host the Runtime contract, and deploy. The agent code never changes; only the adapters do.
This is the practical companion to the AWS Bedrock AgentCore overview. The through-line is the adapter pattern: you build and prove the agent locally with mock adapters, then swap to AgentCore one line at a time — the agent logic in between never changes.
What's agentfootprint vs. what's you
agentfootprint owns the agent + the data-plane adapters (steps 1–8), including the
Runtime container contract since 7.15.0. AWS provisioning (control plane — every
Create*) is yours (step 9).
Prerequisites
- An AWS account with Bedrock model access (e.g.
us.anthropic.claude-sonnet-4-5), a region (us-west-2here). - The AgentCore resources provisioned (control plane — Memory, WorkloadIdentity, Gateway). Do this with the AWS SDK or CDK; you'll reference them by id/ARN below.
- Node 20+.
Step 1 — Install
npm install agentfootprint
# optional AWS SDK peers — install only the adapters you use:
npm install @aws-sdk/client-bedrock-runtime # bedrock() LLM (Converse)
npm install @aws-sdk/client-bedrock-agentcore # AgentCoreStore (memory) + agentCoreIdentity
npm install @aws-sdk/client-cloudwatch-logs # agentcoreObservabilityThe core never imports a vendor SDK — each adapter declares its AWS client as an optional peer, so the bundle stays lean and you pull only what you wire.
Step 2 — Build it mock-first ($0, offline)
Prove the whole agent with dev adapters before touching AWS. This is the exact code path you ship — only the adapters change later.
import { Agent, defineTool } from 'agentfootprint'
import { mock } from 'agentfootprint/providers'
import { defineMemory, MEMORY_TYPES, MEMORY_STRATEGIES, InMemoryStore } from 'agentfootprint/memory';
const weather = defineTool({
name: 'weather',
description: 'Get current weather for a city.',
inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
execute: async (a) => `${(a as { city: string }).city}: sunny, 72°F`,
});
const memory = defineMemory({
id: 'conversation',
type: MEMORY_TYPES.EPISODIC,
strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
store: new InMemoryStore(), // ← dev adapter
});
export function buildAgent(provider = mock({ replies: [
{ toolCalls: [{ id: '1', name: 'weather', args: { city: 'San Francisco' } }] },
{ content: 'San Francisco: sunny, 72°F.' },
]}) ) {
return Agent.create({ provider, model: 'mock', maxIterations: 8 })
.system('You answer weather questions using the `weather` tool.')
.tool(weather)
.memory(memory)
.build();
}const agent = buildAgent();
console.log(await agent.run({ message: 'Weather in SF?', identity: { conversationId: 'c1' } }));Step 3 — Real model: Bedrock
Swap mock() → bedrock(). One line.
import { bedrock } from 'agentfootprint/providers';
const provider = bedrock({ region: 'us-west-2', model: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0' });
const agent = buildAgent(provider);Streaming + tools
bedrock().stream() returns toolCalls: [] (it streams text but doesn't reconstruct
tool_use from deltas). For a tool-using agent, use complete() (non-streaming) until
delta reconstruction lands, or your first tool call will be dropped. See the
overview gotcha.
Step 4 — Memory on AgentCore
Swap InMemoryStore → AgentCoreStore pointed at your Memory resource. One line — the
defineMemory(...) strategy and the agent are unchanged.
import { AgentCoreStore } from 'agentfootprint/memory';
const memory = defineMemory({
id: 'conversation',
type: MEMORY_TYPES.EPISODIC,
strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
store: new AgentCoreStore({ memoryId: process.env.AGENTCORE_MEMORY_ID!, region: 'us-west-2' }),
});AgentCore Memory is an append-only event log: put appends a CreateEvent, list is
ListEvents, and get/delete by id are list-then-find (there's no DeleteSession, so
forget lists + deletes each event). The identity maps to AgentCore's actorId
(tenant/principal) + sessionId (conversationId). WINDOW / episodic memory is the natural
fit (append + list recent).
For server-side semantic retrieval, store.search() wraps RetrieveMemoryRecords — pass the
query as text as well as a vector, because AgentCore ranks on its own side:
await store.search(identity, queryVector, { text: 'where does Ada like to sit?', k: 5 });Deep dive: Memory store adapters · AgentCore adapters.
Step 5 — Identity
Swap dev credentials → managed AgentCore identity. One line, no tool-code change.
import { agentCoreIdentity } from 'agentfootprint/security';
const credentials = agentCoreIdentity({
region: 'us-west-2',
workloadName: 'workflow_assistant_agent',
userIdFor: ({ principal }) => principal,
});
Agent.create({ provider, model, credentials }) /* … */ .build();Step 6 — Tools via a Gateway
If your tools live behind an AgentCore Gateway (Lambda / OpenAPI / MCP target), consume its MCP endpoint through the Tools port:
import { agentCoreIdentity } from 'agentfootprint/security';
import { gatewayTransport, mcpClient, gatedTools, staticTools } from 'agentfootprint/providers';
const gateway = await mcpClient({
name: 'gateway',
transport: gatewayTransport({
url: process.env.GATEWAY_MCP_URL!,
credentials: agentCoreIdentity({ region: 'us-west-2' }),
service: 'gateway',
}),
});
const tools = gatedTools(staticTools(await gateway.tools()), (n) => allowed.has(n));
Agent.create({ provider }).toolProvider(tools).build();gatewayTransport vends the auth headers per request, so a long-running agent never
outlives its bearer token. The token is used once and never stored.
Policy needs no step here. AgentCore enforces it at the Gateway, in front of the tool: a
denial arrives as an MCP error on the tool call and lands in the loop as that tool's result,
which the model reads and adapts to. (agentCorePolicy() is retired in 9.4.0 — it dispatched
EvaluatePolicyCommand, which AgentCore does not have.) For rules you own, the same
permissionChecker port takes a local policy:
import { PermissionPolicy } from 'agentfootprint/security';
Agent.create({
provider,
model,
permissionChecker: PermissionPolicy.fromRoles({ readonly: ['lookup'] }, 'readonly'),
}).build();Step 7 — Observability
import { agentcoreObservability } from 'agentfootprint/observe';
import { microtaskBatchDriver } from 'footprintjs/detach';
agent.enable.observability({
strategy: agentcoreObservability({ region: 'us-west-2', logGroupName: '/agentfootprint/assistant' }),
detach: { driver: microtaskBatchDriver, mode: 'forget' },
});The full breakdown — AgentCore Observability and OTEL, multi-exporter, the OTLP tracer wiring — is in Monitoring: Exporters: AgentCore & OTEL.
Step 8 — Host the Runtime contract
AgentCore Runtime calls a container over a fixed HTTP contract: GET /ping (health),
POST /invocations (run) on port 8080, and the caller's conversation in the
X-Amzn-Bedrock-AgentCore-Runtime-Session-Id header. That contract is
agentCoreRuntimeHost(), so this step is no longer yours to write:
import { standingAgent } from 'agentfootprint/hosting';
import { agentCoreRuntimeHost, agentCoreSessions } from 'agentfootprint/hosting';
const handle = await standingAgent({
agent: buildAgent(provider),
host: agentCoreRuntimeHost(), // 0.0.0.0:8080, /invocations, /ping
sessions: agentCoreSessions({ store: 'session-storage' }), // survives a stop/resume
});
process.on('SIGTERM', () => void handle.close());standingAgent hydrates the session, resumes that conversation or starts a fresh one,
persists what the run left behind, then replies — so a second call with the same session id
continues the conversation. Swap the store for
agentCoreSessions({ store: 'memory', memoryId }) to outlive the session entirely.
The runnable version of this file is examples/deploy/agentcore-runtime.ts; the adapter
reference is AgentCore adapters.
Step 9 — Provision & deploy (control plane — your CDK/SDK)
This part is AWS, not agentfootprint: package the host as a container, then
CreateAgentRuntime / UpdateAgentRuntime to point AgentCore at it (and create the Memory,
WorkloadIdentity, and Gateway resources you referenced above). Use the AWS SDK
(@aws-sdk/client-bedrock-agentcore-control) or CDK. See the
AgentCore docs.
Step 10 — Verify
Invoke the deployed runtime (InvokeAgentRuntime, SigV4) — or hit your host directly while
testing — and confirm a two-turn conversation recalls across turns (memory) and that the
tool fires.
runtimeSessionId must be at least 33 characters
The service validates the length, so a tidy "c-1" is rejected before your container sees
anything. Send a UUID (or your own id with a stable prefix) — it arrives as the
X-Amzn-Bedrock-AgentCore-Runtime-Session-Id header and becomes HostRequest.sessionId.
A second, unrelated fact from the same deployment: a direct-code (zip / NODE_22)
runtime serves /ws fine, even though the vendor docs describe WebSocket support for
container deployments only.
Locally:
const a = buildAgent(provider);
const id = { conversationId: 'verify-1' };
await a.run({ message: 'My order id is 4471.', identity: id });
console.log(await a.run({ message: 'What was my order id?', identity: id })); // → recalls 4471Recap — what changed vs. what didn't
| Step | What you swapped | Lines changed |
|---|---|---|
| 3 | LLM → bedrock() | 1 |
| 4 | store → AgentCoreStore | 1 |
| 5 | creds → agentCoreIdentity() | 1 |
| 6 | tools → mcpClient({ transport: gatewayTransport() }) | 1 |
| 7 | observability → agentcoreObservability() | 1 |
| 8 | host → agentCoreRuntimeHost() + agentCoreSessions() | ~6 |
| 9 | provision + deploy (control plane) | yours |
The agent's logic — system prompt, tools, memory strategy, ReAct loop — never changed.
That's the adapter pattern paying off: the same agent runs on mock +
InMemoryStore locally and on Bedrock + AgentCore in production.
AWS & Bedrock AgentCore
The first worked provider. Every AWS service agentfootprint adapts — Memory, Identity, Runtime hosting, Gateway MCP, Observability, Policy, S3 Vectors, Bedrock models and embedders — with its adapter, its door, its peer dependency, the SDK commands it dispatches, and its honest status.
AgentCore adapters — runtime, sessions, policy, gateway
agentCoreRuntimeHost, agentCoreSessions, agentCorePolicy and gatewayTransport — the AgentCore adapter set on ports that already existed, plus a plain statement of which parts are really verified and which are contract-mapped.
