BuildInfrastructureAWS

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–7). AWS provisioning (control plane) and the Runtime host are yours (steps 8–9) — there is no JS harness, so those steps are wiring, not library calls.

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, mock, defineTool, defineMemory, MEMORY_TYPES, MEMORY_STRATEGIES, InMemoryStore } from 'agentfootprint';

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/llm-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-providers';

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); server-side semantic retrieve (RetrieveMemoryRecords) isn't wrapped yet. Deep dive: Memory store adapters.

Step 5 — Identity

Swap dev credentials → managed AgentCore identity. One line, no tool-code change.

import { agentCoreIdentity } from 'agentfootprint/identity';

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 { mcpClient, gatedTools } from 'agentfootprint/tool-providers';

const gateway = mcpClient(process.env.GATEWAY_MCP_URL!);
Agent.create({ provider }).toolProvider(gatedTools(gateway, (n) => allowed.has(n))).build();

Step 7 — Observability

import { agentcoreObservability } from 'agentfootprint/observability-providers';
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 (the bridge)

AgentCore Runtime calls a container over a fixed HTTP contract: GET /ping (health) and POST /invocations (run) on port 8080. There's no JS harness, so you write a tiny host around agent.run():

import { createServer } from 'node:http';

const agent = buildAgent(provider);

createServer(async (req, res) => {
  if (req.method === 'GET' && req.url === '/ping') {
    res.writeHead(200).end('OK');
    return;
  }
  if (req.method === 'POST' && req.url === '/invocations') {
    let body = '';
    for await (const chunk of req) body += chunk;
    const { message, sessionId } = JSON.parse(body || '{}');
    const result = await agent.run({ message, identity: { conversationId: sessionId ?? 'default' } });
    res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ result }));
    return;
  }
  res.writeHead(404).end();
}).listen(8080);

(Use Fastify/Express if you prefer — the contract is just those two routes.)

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. 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() gateway1
7observability → agentcoreObservability()1
8–9host + deploy (the bridge)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