Build

Infrastructure — ports & adapters

agentfootprint is framework-side. Everything that touches your infrastructure — memory, identity, hosting, tools, telemetry — is a PORT with swappable ADAPTERS. This is the translation boundary, the one decision table, and the honest status of every adapter that ships.

The agent loop is infrastructure-agnostic on purpose. agentfootprint owns the reasoning — context engineering and the ReAct loop — and exposes a small set of ports for everything that touches your infrastructure. An adapter implements one port for one backend. Swap the adapter, keep the agent.

This is the classic ports-&-adapters pattern

Core stays free of vendor SDKs; each adapter is a thin shim around one provider. The agent depends on the interface, never the implementation — so a vendor swap never reaches your agent code.

The translation boundary

Three layers, and keeping them apart is the whole design.

A port is written in the vocabulary every backend already has — an identity, an entry, a cursor, a request, a reply, a session id, an event. Nothing else. The hosting ports say it out loud:

The rule these types are written under: no runtime, product or protocol gets a field, a name or an assumption here. A port shaped around one provider's request envelope stops being a port and becomes that provider's SDK with extra steps, and every later adapter pays for it.

An adapter is the vendor layer, and the only place a vendor SDK is allowed to appear. It is lazily required, declared as an optional peer dependency, and refuses by name when its SDK is missing — so the main bundle stays lean and you install only what you use.

A strategy is the choice you make inside a port once the adapter is picked: which memory strategy runs, which retrieval rule ranks, which detach driver ships telemetry, which durability mode writes a session. Strategies are about behaviour; adapters are about backends. Confusing them is how a "swap the database" ticket becomes a rewrite.

The ports

PortInterfaceQuestion it answersDoor
Memory storeMemoryStoreWhere do entries and vectors live?agentfootprint/memory
CredentialsCredentialProviderHow does a tool get a token to call something?agentfootprint/security
PermissionPermissionCheckerIs this tool call allowed?agentfootprint/security
Host / conversationAgentHost, ConversationHostWhat carries requests to the agent?agentfootprint/hosting
SessionSessionLifecycleWhere does the conversation live between requests?agentfootprint/hosting
ToolsMcpClient, ToolProviderWhere do tools come from, and which are visible?agentfootprint/providers
TelemetryObservabilityStrategyWhere does the typed event stream go?agentfootprint/observe
LLMLLMProviderWhich model backend answers?agentfootprint/providers
EmbedderEmbedderWhat turns text into a vector?agentfootprint/providers

The agent wires ports, not vendors:

const agent = Agent.create({ provider, model, credentials }) // LLM + credential ports
  .memory(defineMemory({ store }))                            // memory-store port
  .toolProvider(tools)                                        // tools port
  .build();

One decision table

Start from what you are trying to do, not from which cloud you are on.

You need…Reach forWhy that one
A fixed corpus on a runtime with no disk (serverless, edge, immutable image)staticVectorStore(bundle) — or s3VectorsStore if it must change without a redeployThe bundle is plain JSON built where the credentials live; S3 Vectors is durable object storage with a native index
A durable corpus on one machinesqliteVectorStore({ file })Node's built-in node:sqlite — nothing to install, exact cosine, survives a restart. Documented ceiling: 50,000 chunks (guidance from measurements, not an enforced limit)
A corpus beside your own datapgVectorStoreInherits the backups, failover, access control and migrations you already run
Conversation memory in productionAgentCoreStore or RedisStore via defineMemory({ store })Managed event log, or sub-ms hot reads with TTL. Neither ranks your vectors — see the ranking axis
Conversations to survive a restartsqliteSessions({ file }) as SessionLifecycleConversations and paused runs in one table, zero dependencies
An agent that stays up and answers HTTPstandingAgent({ agent, sessions, host }) with nodeHost()The composer: hydrate → resume-or-fresh → persist → reply
A container runtime's own HTTP contracthttpHost({ wire }), or agentCoreRuntimeHost()Two paths and five body shapes are all a second HTTP adapter re-decides
Per-request auth on a remote tool servergatewayTransport({ url, credentials })Headers vended inside every fetch, so a standing agent never outlives its token
A signed request (SigV4, DPoP, HMAC)McpHttpTransport.fetch — your own functionA signature is computed from the request, so it cannot be decided at connect time
Consent and spend gates before a consequential actioncheckIn on the tool + a CredentialProvider; ask in .toolMiddleware()A pause puts the exact operation in front of a person, with its evidence pack
A role allowlist you ownPermissionPolicy.fromRoles(roles, activeRole)In-process, fail-closed, refusal-as-data — the model reads a bracketed denial it can adapt to
To audit who did whatrun({ identity }) + any envelope-serializing sinkEventMeta.principal / .tenant ride every event of the run — stamped only from an identity a caller NAMED, absent otherwise
Platform-authenticated users — the person is known at the front door, not in your codeagentCoreRuntimeWire's X-Amzn-Bedrock-AgentCore-Runtime-User-IdHostRequest.userId, and getCredential({ userToken }) for the JWT exchangeThe runtime forwards WHO; standingAgent makes them the run's principal; AgentCore Identity exchanges their own token for a per-(workload, user) one. The header is read by that adapter only — a generic container's headers are not identity
To govern what a tool touches, not just its nameTool.capabilities + PermissionPolicy.fromRoles(…, { capabilities })Enforced where both sides declare it; the framework never guesses a tool's reach
One role to see a smaller skill catalogPermissionPolicy.fromRoles(…, { skills })The row leaves the read_skill menu AND the activation is refused, from one rule
A ceiling on one tool resultAgent.create({ maxToolResultChars })Over the cap the model reads a marker that names the size and says to narrow the request — opt-in, because a default would silently modify results
Events beside your runtime telemetryagentcoreObservability() / cloudwatchObservability() / otelObservability()The typed event stream, shipped in the shape that platform already reads
Gemini's own numbers — cached and reasoning tokens as separate line items, and tools that stay JSON Schemagemini({ project, location }) (or { apiKey })The native SDK reports cachedContentTokenCount and thoughtsTokenCount, and takes parametersJsonSchema untranslated. Google's OpenAI-compatible endpoint has none of that, and its token expires hourly
Traces in Cloud TraceotelObservability() pointed at telemetry.googleapis.comGoogle accepts standard OTLP and recommends it over their own exporter; our gen_ai.* attributes are what their console renders
Telemetry where there is no collector to ship tofileObservability({ path })NDJSON on a local disk, one line per event — the format every log shipper already reads, and zero dependencies
Tokens out of the vault you already runvaultCredentials({ address })KV v2 over Vault's own HTTP API — no SDK, and the token never reaches an error message
Two sinks at oncecomposeObservability([a, b])One strategy that fans out; flush and stop reach both
A tamper-evident recordauditExport()Hash-chained records; verifyAuditBundle names the exact record that broke

Dev → prod is one line per port

The promise: mock-first locally, real infra in prod, and the agent code in between never changes.

// dev — $0, offline, deterministic
const store = new InMemoryStore();
const provider = mock({ replies: [/* scripted */] });

// prod — same agent, real infra (just these two lines change)
const store = new AgentCoreStore({ memoryId: process.env.AGENTCORE_MEMORY_ID! });
const provider = bedrock({ model: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0' });

How each page below is laid out

Every capability page follows the same four parts, so you can skim to the one you need:

  1. The port — the exact members, and which are optional and feature-detected.
  2. The adapters — what ships, which peer dependency, and what each one genuinely does not do.
  3. The strategy axes — the choices left after the adapter is picked, with a choose-when row for each.
  4. Status — shipped, or the honest label. Nothing is called verified that has not been.

What the status labels mean

Shipped means it works and is tested. Where an adapter's calls are exercised through an injected client seam rather than against the real service, the page says contract-mapped and injection-tested and names it. A brand-new adapter whose refusals and behaviour are pinned by tests but which has not yet met a real backend says contract-shaped and tested; awaiting field use.

Field-validated is the next rung up, and it is claimed only with its evidence named: the adapter answered a real request from the real service, in a live account, exercised by someone who did not write it. Today that phrase reads validated in an independent field trial, 2026-08 and points at one trial on live Google Cloud — the rows it covers are on Google Cloud & Gemini, plus fileObservability, which that trial also wrote and read back. Where a trial exercised the service but not this library's adapter, the page says so instead of borrowing the credit.

Above it, and reserved: only a claim that has been seen working in a real deployment, under sustained traffic, is written as verified in a production field deployment — and no adapter carries that phrase. The one place it appears is On-premises & self-hosted, where it describes a deployment shape — a standing agent over httpHost + sqliteSessions against an OpenAI-compatible gateway — and not any single adapter's feature list.

Capability pages

  • Memory & stores — the MemoryStore port, seven adapters, and what each search() actually ranks
  • Sessions in a filesqliteSessions, the zero-dependency SessionLifecycle
  • Identity & credentials — the CredentialProvider port, machine vs user, consent
  • Governance & policy — who is allowed to say no, and where each refusal is enforced
  • Hosting & runtime — hosts, sessions, durability, and the human-in-the-loop resume loop
  • Tools & gateways — the McpClient port and its four transport choices, plus the CodeRunner port: summarize prose, compute data — big tool DATA gets computed outside the context window instead of pasted into it
  • Observability sinks — the ObservabilityStrategy port, composition, and delivery timing

Providers

  • AWS & Bedrock AgentCore — the first worked provider: every service, its adapter, its door, its peer dependency, and its honest status.
  • Google Cloud & Gemini — the third column: the native Gemini provider and Gemini embeddings ship, Cloud Trace is a recipe over standard OTLP, and every other boundary names the port that fills it and the reason it is not an adapter yet.
  • On-premises & self-hosted — the column for a deployment that owns its own machines: local models, stores on your disk, NDJSON telemetry, your vault. There is no onPremises() adapter, because on-premises is the absence of a vendor rather than one more of them.

More clouds plug in the same way — each is a set of adapters behind the same ports. Nothing about the ports above knows AWS exists, and nothing about them requires a cloud at all.

On this page