Build

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.

Amazon Bedrock AgentCore is AWS's managed runtime + primitives for agents (Memory, Identity, Gateway, Observability, Code Interpreter, Browser). agentfootprint is framework-side — it ships adapters for the data-plane primitives and consumes a Gateway over MCP. You bring the control plane (provisioning) and the runtime host; everything else is a port you fill with an AgentCore adapter.

Prefer hands-on?

Follow AgentCore: step by step — build mock-first, then swap to AgentCore one adapter (one line) at a time. This page is the reference map behind it.

Two AgentCore surfaces

Control plane (bedrock-agentcore-control) creates resources (CreateMemory, CreateGateway, CreateWorkloadIdentity, CreateAgentRuntime, …) — do this with the AWS SDK / CDK. Data plane (bedrock-agentcore) uses them at runtime — this is what agentfootprint's adapters wrap.

Service → adapter map

Every row: what the adapter is called, which door exports it, which peer dependency it lazily requires, which SDK commands it dispatches, and what its status honestly is.

AWS serviceAdapterDoorPeer depDispatchesStatusCapability page
AgentCore MemoryAgentCoreStoreagentfootprint/memory@aws-sdk/client-bedrock-agentcoreCreateEvent, ListEvents, DeleteEvent, RetrieveMemoryRecordsShipped; contract-mapped and injection-testedMemory & stores
Bedrock Agents memory (prior generation)BedrockAgentMemory — a reader, not a storeagentfootprint/memory@aws-sdk/client-bedrock-agent-runtimeGetAgentMemory, DeleteAgentMemoryShipped; contract-mapped and injection-testedMemory & stores
AgentCore IdentityagentCoreIdentityagentfootprint/security@aws-sdk/client-bedrock-agentcoreGetWorkloadAccessTokenForJWT, GetWorkloadAccessTokenForUserId, GetResourceOauth2Token3 of the 6 Identity data-plane operationsShipped; contract-mapped and injection-tested. The JWT exchange (9.12.0) is contract-shaped and tested; awaiting field useIdentity & credentials
AgentCore Runtime (hosting)agentCoreRuntimeHost, agentCoreRuntimeWireagentfootprint/hostingnone — plain HTTPShipped — really verified: passes the same host conformance suite as nodeHost over a real socketHosting & runtime
AgentCore Runtime (per-user identity)agentCoreRuntimeWire reads X-Amzn-Bedrock-AgentCore-Runtime-User-IdHostRequest.userId → the run's identity.principalagentfootprint/hostingnone — a headerShipped (9.12.0) — really verified: the header mapping is asserted over a real socket, and the generic wire is asserted NOT to read itIdentity & credentials
AgentCore Runtime (sessions)agentCoreSessions({ store })agentfootprint/hosting@aws-sdk/client-bedrock-agentcoreonly for store: 'memory'CreateEvent, ListEventsShipped; the file mode uses node:fs onlyHosting & runtime
AgentCore GatewaygatewayTransport + mcpClientagentfootprint/providers@modelcontextprotocol/sdk— (MCP over Streamable HTTP)ShippedTools & gateways
AgentCore ObservabilityagentcoreObservabilityagentfootprint/observe@aws-sdk/client-cloudwatch-logsCreateLogStream, PutLogEventsShipped; contract-mapped and injection-testedObservability sinks
CloudWatch Logs (generic)cloudwatchObservabilityagentfootprint/observe@aws-sdk/client-cloudwatch-logsCreateLogStream, PutLogEventsShipped; contract-mapped and injection-testedObservability sinks
X-RayxrayObservabilityagentfootprint/observe@aws-sdk/client-xrayPutTraceSegmentsShipped; contract-mapped and injection-testedObservability sinks
AgentCore PolicyagentCorePolicyagentfootprint/securitynothingRetired 9.4.0 — refuses at constructionGovernance & policy
S3 Vectorss3VectorsStoreagentfootprint/memory@aws-sdk/client-s3vectorsGetIndex, PutVectors, QueryVectors, GetVectors, ListVectors, DeleteVectorsShipped (9.3.0); contract-mapped and injection-testedMemory & stores
S3 (artifact store)s3Artifactsagentfootprint@aws-sdk/client-s3PutObject, GetObject, HeadObject, DeleteObject, ListObjectsV2 — plus native putStream / getStream9.25.0 — contract-shaped and tested; awaiting field useArtifacts
Bedrock modelsbedrock() — Converse, model-agnosticagentfootprint/providers@aws-sdk/client-bedrock-runtimeConverse, ConverseStreamShippedAWS Bedrock
Bedrock embeddingsbedrockEmbedder()agentfootprint/providers@aws-sdk/client-bedrock-runtimeInvokeModelShippedEmbedders
Actor on every event (EventMeta.principal / .tenant)run({ identity }) — no adapteragentfootprint (main)noneShipped (9.11.0); identical on both provider columnsObservability sinks
Control plane (all Create*)🔴 Bridge it yourself — AWS SDK or CDK
Code Interpreter / Browser🔴 Bridge it — wrap as a defineTool or an MCP toolTools

What each status label means, exactly

Really verified is used for one row only: agentCoreRuntimeHost is plain HTTP with no AWS SDK on its path, and it passes the same host conformance suite as nodeHost over a real socket.

Contract-mapped and injection-tested means the adapter's AWS calls are exercised through its _client / _sdk seams — never against AWS. The command names are pinned by test (below); the field semantics are read from the API docs. Confirm them against your installed SDK. No adapter on this page — and no adapter anywhere on this site — is described as verified in a production field deployment. That phrase appears in one place only, On-premises & self-hosted, where it describes a deployment shape rather than an adapter.

The command-name pin

Every AWS adapter's dispatched SDK command constructors are pinned by test in test/adapters/aws/awsCommandPin.ts, and a completeness assertion fails the build for any src/** file that loads an @aws-sdk/* package without a row. Adding or renaming a dispatched command means editing that row — it cannot be done quietly.

The rule the registry exists to enforce is one line: a bare @aws-sdk/client-* client is command-based. Its prototype is exactly [constructor, destroy] plus send — per-operation method shortcuts live on the aggregated client, so client.someOperation(...) is always wrong here.

Three adapters shipped violating it, which is why the registry exists:

VersionThe defect
6.42.0An adapter dispatched commands belonging to a different service
9.4.0agentCorePolicy dispatched EvaluatePolicyCommand, which does not exist in that package
9.4.0agentCoreIdentity called client.getResourceOauth2Token(...) — a method that is never there, so the documented path failed 100% of the time on the first call

Each compiled, and each passed tests that injected _client past the SDK. The AWS SDKs are deliberately not devDependencies — six adapters prove their missing-peer-dep refusals by real absence — so the real-module half of the suite runs only where the SDKs are installed.

Memory — AgentCoreStore

The store is just the Memory-store port pointed at an AgentCore Memory resource. Its methods map 1:1 to data-plane events; the agent code is identical to the in-memory version.

import { defineMemory, MEMORY_TYPES, MEMORY_STRATEGIES } from 'agentfootprint/memory';
import { AgentCoreStore } from 'agentfootprint/memory';

const store = new AgentCoreStore({
  memoryId: process.env.AGENTCORE_MEMORY_ID!,
  region: 'us-west-2',
});

const memory = defineMemory({
  id: 'conversation',
  type: MEMORY_TYPES.EPISODIC,
  strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
  store,
});

Agent.create({ provider }).memory(memory).build();
  • put/putMany → CreateEvent, list → ListEvents, delete → DeleteEvent. get and delete by id are list-then-find — AgentCore assigns event ids on write, so there is nothing to fetch by — and forget lists then deletes each event, because AgentCore has no DeleteSession. The memory identity (conversationId) maps to the AgentCore sessionId.
  • putIfVersion is emulated client-side; seen / recordSignature / feedback are in-process shadow state and do not survive a restart. Pair with RedisStore for durable recognition.
  • The full memory deep-dive lives in AgentCore Memory; the port and the other six stores are on Memory & stores.
  • search() wraps RetrieveMemoryRecords. AgentCore ranks server-side and takes a text query, so pass options.text alongside the vector — details and the reason in AgentCore adapters.

Identity — agentCoreIdentity

import { agentCoreIdentity } from 'agentfootprint/security';

const credentials = agentCoreIdentity({
  region: 'us-west-2',
  workloadName: 'workflow_assistant_agent',
  userIdFor: ({ principal }) => principal, // per-(workload, user) token vault
});

Agent.create({ provider, model, credentials }).build();

Resolves GetWorkloadAccessTokenForUserId / GetResourceOauth2Token at runtime. Swapping from dev static credentials to managed AgentCore identity is this one line — no tool-code change. Provisioning the providers (CreateWorkloadIdentity, CreateOauth2CredentialProvider) is control-plane → SDK / CDK.

Since 9.12.0 a third operation joins them: pass the caller's own IdP token on the request — getCredential({ service, mode: 'user', userToken }) — and the adapter exchanges it through GetWorkloadAccessTokenForJWT before vending, so the vault entry belongs to the person rather than to the agent. That is the outbound half of per-user identity; the inbound half is X-Amzn-Bedrock-AgentCore-Runtime-User-Id, which agentCoreRuntimeWire reads and standingAgent turns into the run's principal. Both halves, with the flow diagram: Identity & credentials.

Two facts worth carrying into a deployment: this adapter reports no expiresAt (GetResourceOauth2TokenResponse has no expiry field, so there is nothing honest to report), and until 9.4.0 it called a method that is never present on a command-based client — the defect that bought the command-name pin above. The whole story, plus the port it implements: Identity & credentials.

Gateway — tools over MCP

An AgentCore Gateway turns a Lambda / OpenAPI / Smithy / MCP target into a single MCP endpoint with Identity-brokered egress auth. agentfootprint consumes it through the Tools/Gateway port:

import { agentCoreIdentity } from 'agentfootprint/security';
import { gatewayTransport, mcpClient, staticTools, gatedTools } from 'agentfootprint/providers';

const gateway = await mcpClient({
  name: 'gateway',
  // Auth headers vended PER REQUEST, so a standing agent never outlives its token.
  transport: gatewayTransport({
    url: process.env.GATEWAY_MCP_URL!,
    credentials: agentCoreIdentity({ region: 'us-west-2' }),
    service: 'gateway',
  }),
});
const tools = gatedTools(staticTools(await gateway.tools()), (name) => allowed.has(name));

Agent.create({ provider }).toolProvider(tools).build();

agentfootprint does not create Gateways (no control-plane client) — CreateGateway / CreateGatewayTarget are SDK / CDK.

Policy — enforced at the Gateway, not here

There is nothing to attach. AgentCore enforces policy at the Gateway, in front of the tool, before a request reaches your process; a denial comes back as an MCP error on the tool call made through mcpClient(...) and lands in the loop as that tool's result, which the model reads and adapts to.

agentCorePolicy() tried to pre-evaluate the same rule in-process and is retired in 9.4.0: it dispatched EvaluatePolicyCommand, which does not exist in @aws-sdk/client-bedrock-agentcore — AgentCore has no data-plane authorization call at all. The export remains and refuses at construction with the whole explanation.

For rules you own, use PermissionPolicy.fromRoles(...) as the permissionChecker, or the .toolMiddleware() chain for conditional ones. See AgentCore adapters.

Observability — agentcoreObservability

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' }, // keep the loop unblocked
});

Ships the typed AgentfootprintEvent stream to CloudWatch in AgentCore's GenAI-Observability schema. otelObservability() / cloudwatchObservability() / xrayObservability() are the other adapters behind the same port. Full step-by-step (AgentCore Observability and OTEL, multi-exporter, detach): Exporters: AgentCore & OTEL.

Who the run was for (9.11.0)

run({ identity }) puts the caller's principal and tenant on every event of the run (EventMeta.principal / EventMeta.tenant), which is what turns the stream into a who → what → when audit record.

await agent.run(message, {
  sessionId,
  identity: { tenant: 'acme', principal: 'alice@acme.test', conversationId },
});

This is column-independent — the same field, the same rule, the same code path on AWS and on your own machines. The only difference between the columns is which sink is reading it, and the envelope-serializing sinks (agentcoreObservability, cloudwatchObservability, fileObservability, auditExport) all carry it without knowing it exists. otelObservability places it deliberately as two agentfootprint.* span attributes (principal.id and tenant.id) on the run span; xrayObservability does not map it. Stamped only from an identity a caller NAMED — never from a sessionId, never invented. Detail: Observability sinks.

S3 Vectors — a corpus you can add to without a redeploy

Not an AgentCore primitive, but the AWS row a RAG deployment usually needs: object storage with a native vector index and a query API, priced like storage rather than like a database cluster.

import { s3VectorsStore } from 'agentfootprint/memory';
import { bedrockEmbedder } from 'agentfootprint/providers';

const store = s3VectorsStore({ bucket: 'my-corpus', index: 'docs', region: 'us-east-1' });
const embedder = bedrockEmbedder({ region: 'us-east-1' });

search() maps onto QueryVectors, put/putMany onto PutVectors. Since 9.4.0 the store issues one memoized GetIndex before it trusts the index, and refuses a metric, a metadata layout or a dimension it cannot serve honestly — which needs the s3vectors:GetIndex IAM permission alongside the others. Create the index yourself; this store never does. Full story, including the nonFilterableMetadataKeys: ["af"] requirement: Memory & stores.

S3 — the artifact store (s3Artifacts, 9.25.0)

The claim-check store in an S3 bucket: tools check big results in and the model routes ~26-char tickets. Same five-verb port as every other adapter, so this is a one-line swap from fileArtifacts.

import { s3Artifacts } from 'agentfootprint';

const artifacts = s3Artifacts({
  bucket: 'my-agent-artifacts',   // must already exist — this library never creates one
  prefix: 'artifacts',            // optional, so a bucket can be shared
  region: 'us-east-1',
});

const agent = Agent.create({ provider, artifacts });

S3ArtifactsOptions also takes client (your own pre-built S3Client, so configuration and credentials stay yours) and retention. That same client option is how an S3-compatible on-prem store (MinIO and the like) plugs in — build the S3Client against your own endpoint and path style and pass it in; the adapter dispatches the same five commands either way.

The object key is [<prefix>/]<tenant>/<principal>/<conversation>/<ref> — the scope partitioned into key segments with the same percent-encoding law the directory adapter uses, so a tenant of literally .. is a name, not a navigation hop. A ref alone opens nothing: a wrong scope computes a different key, S3 answers 404, and the caller reads null — one indistinguishable miss, never a cross-tenant read.

The ticket rides as object metadata (x-amz-meta-af-artifact, one entry, ASCII JSON), not a sidecar object. That makes head() exactly one HeadObject and get() one GetObject returning ticket and bytes — a sidecar would double every read and open a window where one exists and the other does not. The cost is S3's 2 KB user-metadata cap, checked at put and refused by name (a ticket only reaches it if the label is prose or parentRefs is a bibliography — the refusal says which). The payload is the object body as canonical bytes, so a stored report is downloadable from the console and is the report.

What a listing costs, plainly: ListObjectsV2 returns keys, sizes and LastModified but never user metadata — so list() pages the keys, sorts newest-first, and issues one HeadObject per row it returns, not for the whole scope. An expired object inside a page is dropped from that page rather than backfilled, so a page can come back short while more pages remain. That is the honest trade; an index object listing every ticket would be cheaper and would be a lost-update race between two writers.

Retention, and the operator's bulk tool. ttlMs stamps expiresAt at mint (stated, never sprung) and expiry is enforced on read — an expired object answers null and is swept on the way past, so an expired ticket can never be redeemed. Budgets evict oldest-first (S3 has no cheap read-recency). A put with no budget dials configured is exactly one PutObject — no scope scan, because paying for a full listing on every write would be a real bill for no answer.

Reclaiming storage nobody reads again is what S3 Lifecycle rules are for. They are the operator's bulk tool; this adapter never creates one (a lifecycle rule is a cost and compliance decision that belongs to your infrastructure). Align them so the rule is longer than the store's ttlMs, never shorter — the store's expiresAt is the promise consumers read on the ticket, and a lifecycle rule that deletes first makes a live ticket resolve to null before the time it printed:

{ "Rules": [{ "ID": "agentfootprint-artifacts", "Status": "Enabled",
              "Filter": { "Prefix": "artifacts/" },
              "Expiration": { "Days": 7 } }] }

Streaming is native here: putStream / getStream ride the SDK's own streaming upload and its response-stream mixin. Feature-detect before calling — see Artifacts.

Failures never leak the key. An SDK error echoes the bucket and the object key — and the key carries this run's tenant, principal and conversation. So the SDK's text is withheld; what comes through is the operation, the exception's name and the HTTP status. Every failure, including a 404: the only 404 that comes back raw is one from a call that named one object (head, get, delete), and that one never leaves the adapter, because the line that asked converts it to null on the spot.

A 404 is not automatically "no data". S3 says 404 for two different sentences, and they are the same status: NoSuchKey (that object is not there) and NoSuchBucket (this store is pointed at a bucket that does not exist). So the adapter reads the code, not the status — a missing bucket raises, on every verb, rather than reporting an empty scope over a configuration mistake. A 404 from a write or a listing is an error for the same reason: those calls never asked whether one object was there, so nothing is standing by to read the answer as null.

IAM: s3:PutObject, s3:GetObject, s3:DeleteObject and s3:ListBucket on the prefix. Peer dependency @aws-sdk/client-s3, loaded lazily at construction — a missing install refuses where the config was written, and a browser bundle never sees it.

Bedrock — models and embeddings

bedrock() is model-agnostic through the Converse API: one adapter covers Claude, Llama, Mistral, Titan and Mixtral. Tool calling works on both paths — complete() reads toolUse blocks off the response, and stream() accumulates the ConverseStream contentBlockStart / contentBlockDelta / contentBlockStop events keyed by contentBlockIndex, so parallel tool calls that interleave are reassembled correctly. A parity test pins the two paths together.

Honest by construction

A response with stopReason: 'tool_use' and zero parsed tool calls is never returned quietly: the provider throws a BedrockProviderError with code: 'BEDROCK_STREAM_TOOLUSE_LOST' so a reliability rule can act, instead of the agent confidently answering without running its tools. Applied on both exits, so the two paths cannot drift apart again. (Streamed tool calls were genuinely dropped before 7.6.1 — that is history now, not current behaviour.)

Documented limits: multi-modal is not exposed (text content only), and Bedrock Guardrails are not wrapped — pass them via the SDK client directly. Details: AWS Bedrock.

bedrockEmbedder() speaks per-model body shapes (Titan V2/V1, Cohere Embed English / Multilingual v3) and declares maxInputChars — 32,000 for Titan's 8,192-token window, 2,000 for Cohere, which batches 96 texts per call. See Embedders.

Hosting — where the parallelism comes from

From the platform. AgentCore Runtime is session-oriented: the runtime gives a session its own container and keeps that container's storage for the life of the session, so two sessions are two containers and never meet. That is the first row of the concurrency table — platform-per-session — and it is why the ordinary AgentCore deployment is standingAgent({ agent }) with one agent inside one container:

const handle = await standingAgent({
  agent,
  host: agentCoreRuntimeHost(),                              // 0.0.0.0:8080
  sessions: agentCoreSessions({ store: 'session-storage' }),
});

The global serialization that shape carries costs nothing here, because there is only ever one session in the container to serialize. { agentFactory } — the per-session agent pool — is for the other shape: one process serving many sessions, which is what you have on your own infrastructure or behind your own load balancer, not what AgentCore hands you. Using it inside an AgentCore container is not wrong, just unnecessary: the pool would hold one entry.

What you bridge yourself

agentfootprint is runtime-only, so these are yours:

  • Control plane — every Create* (Runtime, Memory, Gateway, WorkloadIdentity, policy store) via SDK / CDK.
  • Code Interpreter, Browser — wrap as tools (defineTool or an MCP server) for now.

The Runtime host used to be on this list. It is not any more — agentCoreRuntimeHost() + agentCoreSessions() are the :8080 /ping + /invocations contract, including the session header. See AgentCore adapters.

Other providers

AWS is one column, not the pattern. Every port on this page is filled by something else somewhere else, and the agent code does not change between them: On-premises & self-hosted is the same map for a deployment that owns its own machines — a local model instead of Bedrock, sqliteVectorStore or pgVectorStore instead of S3 Vectors, sqliteSessions instead of AgentCore sessions, fileObservability instead of CloudWatch, vaultCredentials instead of AgentCore Identity.

Google Cloud & Gemini is the third column: gemini() instead of bedrock(), geminiEmbedder() instead of bedrockEmbedder(), and otelObservability() over standard OTLP instead of CloudWatch. It is a younger column and says so — two adapters ship, and every other boundary names the port that fills it plus the reason it is not an adapter yet.

Next steps

On this page