InfrastructureAWS

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-2 here).
  • 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          # agentcoreObservability

The 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 InMemoryStoreAgentCoreStore 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 4471

Recap — what changed vs. what didn't

StepWhat you swappedLines changed
3LLM → bedrock()1
4store → AgentCoreStore1
5creds → agentCoreIdentity()1
6tools → mcpClient({ transport: gatewayTransport() })1
7observability → agentcoreObservability()1
8host → agentCoreRuntimeHost() + agentCoreSessions()~6
9provision + 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.

On this page