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
| Port | Interface | Question it answers | Door |
|---|---|---|---|
| Memory store | MemoryStore | Where do entries and vectors live? | agentfootprint/memory |
| Credentials | CredentialProvider | How does a tool get a token to call something? | agentfootprint/security |
| Permission | PermissionChecker | Is this tool call allowed? | agentfootprint/security |
| Host / conversation | AgentHost, ConversationHost | What carries requests to the agent? | agentfootprint/hosting |
| Session | SessionLifecycle | Where does the conversation live between requests? | agentfootprint/hosting |
| Tools | McpClient, ToolProvider | Where do tools come from, and which are visible? | agentfootprint/providers |
| Telemetry | ObservabilityStrategy | Where does the typed event stream go? | agentfootprint/observe |
| LLM | LLMProvider | Which model backend answers? | agentfootprint/providers |
| Embedder | Embedder | What 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 for | Why 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 redeploy | The bundle is plain JSON built where the credentials live; S3 Vectors is durable object storage with a native index |
| A durable corpus on one machine | sqliteVectorStore({ 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 data | pgVectorStore | Inherits the backups, failover, access control and migrations you already run |
| Conversation memory in production | AgentCoreStore 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 restart | sqliteSessions({ file }) as SessionLifecycle | Conversations and paused runs in one table, zero dependencies |
| An agent that stays up and answers HTTP | standingAgent({ agent, sessions, host }) with nodeHost() | The composer: hydrate → resume-or-fresh → persist → reply |
| A container runtime's own HTTP contract | httpHost({ wire }), or agentCoreRuntimeHost() | Two paths and five body shapes are all a second HTTP adapter re-decides |
| Per-request auth on a remote tool server | gatewayTransport({ 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 function | A signature is computed from the request, so it cannot be decided at connect time |
| Consent and spend gates before a consequential action | checkIn 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 own | PermissionPolicy.fromRoles(roles, activeRole) | In-process, fail-closed, refusal-as-data — the model reads a bracketed denial it can adapt to |
| To audit who did what | run({ identity }) + any envelope-serializing sink | EventMeta.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 code | agentCoreRuntimeWire's X-Amzn-Bedrock-AgentCore-Runtime-User-Id → HostRequest.userId, and getCredential({ userToken }) for the JWT exchange | The 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 name | Tool.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 catalog | PermissionPolicy.fromRoles(…, { skills }) | The row leaves the read_skill menu AND the activation is refused, from one rule |
| A ceiling on one tool result | Agent.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 telemetry | agentcoreObservability() / 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 Schema | gemini({ 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 Trace | otelObservability() pointed at telemetry.googleapis.com | Google 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 to | fileObservability({ 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 run | vaultCredentials({ address }) | KV v2 over Vault's own HTTP API — no SDK, and the token never reaches an error message |
| Two sinks at once | composeObservability([a, b]) | One strategy that fans out; flush and stop reach both |
| A tamper-evident record | auditExport() | 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:
- The port — the exact members, and which are optional and feature-detected.
- The adapters — what ships, which peer dependency, and what each one genuinely does not do.
- The strategy axes — the choices left after the adapter is picked, with a choose-when row for each.
- 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
MemoryStoreport, seven adapters, and what eachsearch()actually ranks - Sessions in a file —
sqliteSessions, the zero-dependencySessionLifecycle - Identity & credentials — the
CredentialProviderport, 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
McpClientport and its four transport choices, plus theCodeRunnerport: summarize prose, compute data — big tool DATA gets computed outside the context window instead of pasted into it - Observability sinks — the
ObservabilityStrategyport, 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.
