Reference

Strategy Pattern Everywhere — port / adapter for vendor integrations

v2.6 cache layer's strategy-registry pattern, generalized to every observability / cost / status / lens vendor integration in v2.8+. AWS-first adapter priority — Bedrock AgentCore Observability → CloudWatch → X-Ray — with non-AWS (OTel, Datadog, Pino) following.

In one line: the v2.6 cache layer (one DSL, N vendor strategies, side-effect-import auto-registration, wildcard fallback) is the right shape for every observability / cost / status / lens vendor integration. v2.8+ generalizes the principle as a unified agent.enable.<group>(opts) + strategy-slot architecture. One contract, many adapters, swap by config.

The lineage

This is the same pattern in 5 different formal traditions:

TraditionNameWhat it is
GoF (1994)Strategy PatternPluggable algorithm; client picks at runtime
GoF (1994)Bridge PatternDecouple abstraction from implementation across N vendor boundaries
Cockburn (2005)Hexagonal / Ports & AdaptersThe DSL is the port; each vendor integration is an adapter
Microsoft (.NET)Provider modelExact same shape: pluggable runtime providers selected by config, default + override
Plotkin & PretnarAlgebraic effectsStrategies are effect handlers; the DSL is the effect signature; composition is handler stacking

The cache layer in v2.6 is an instance. v2.8 makes it the universal architectural pattern across the library.

Why we know it works — v2.6 cache as proof of concept

DSL on the consumer side:
  cache: 'always' | 'while-active' | 'never' | { until: predicate }

Pipeline:
  CacheDecisionSubflow → CacheGate → strategy.prepareRequest → wire format

Strategies (auto-registered via side-effect import):
  AnthropicCacheStrategy   (manual cache_control)
  OpenAICacheStrategy      (auto-cache pass-through)
  BedrockCacheStrategy     (model-aware: Claude → Anthropic-style; else pass-through)
  NoOpCacheStrategy        (wildcard fallback)
  Future: GeminiCacheStrategy

Validated outcome:
  Sonnet Dynamic ReAct: 36,322 → 6,535 input tokens (−82%) end-to-end

Properties the cache layer demonstrated and v2.8 generalizes:

  1. One DSL — consumers write declarative intent, not provider-specific calls
  2. N strategies — auto-registered by side-effect import; agentfootprint/cache-anthropic etc. as subpaths with peer-dep on vendor SDK
  3. Wildcard fallback — unknown provider gets a No-Op strategy that's safe by construction
  4. Hot-path zero-allocation — when the strategy says "no work to do," the runtime skips marker emission entirely
  5. CI-testable with mocks — every strategy ships a mock variant; production swaps in the real one
  6. Lock-in-free — vendor swap is a one-line change in config, not an agent rewrite

AWS adapter map — first ecosystem to support

Library already ships memory-agentcore (@aws-sdk/client-bedrock-agent-runtime peer dep). The AWS pattern is established. The AWS-side observability adapters below shipped in v3.1 — all under the single grouped agentfootprint/observability-providers subpath (one factory per vendor, never a new subpath):

Factory (in observability-providers)AWS servicePeer depWhat it doesStatus
agentcoreObservability(opts)AWS Bedrock AgentCore Observability@aws-sdk/client-cloudwatch-logs (+ @aws-sdk/client-bedrock-agent-runtime, already a peer for memory-agentcore)Pipe events into AgentCore's native observability surface. Consumers already integrated with AgentCore Memory get observability with the same auth pattern.Shipped (v3.1)
cloudwatchObservability(opts)AWS CloudWatch Logs@aws-sdk/client-cloudwatch-logsGeneric structured logging. Standard AWS observability sink.Shipped (v3.1)
xrayObservability(opts)AWS X-Ray@aws-sdk/client-xrayDistributed tracing — spans per LLM call / tool call / iteration. Derives parent-child from runtimeStageId.Shipped (v3.1)
audit (CloudTrail) (future)AWS CloudTrailTamper-evident audit log for permission decisions, denied tools, capability inventory.future, when enable.audit ships
cost (Cost Explorer) (future)AWS Cost & BillingPipe agentfootprint cost ticks into Cost Explorer for org-level dashboards.future

Why AWS-first:

  1. Existing precedentmemory-agentcore already ships, same peer-dep auth pattern reused
  2. AgentCore observability is the lowest-effort, highest-value first adapter — same SDK consumer is already importing
  3. CloudWatch covers the "I don't use AgentCore but I'm on AWS" case — broader audience
  4. X-Ray adds distributed tracing — same pattern OTel uses, but AWS-native (no OTel collector needed)

After AWS lands, OTel + Datadog + Pino follow in v2.9.x (vendor-neutral / non-AWS).

The 4 groups in scope for v2.8

Each group is a port; vendor integrations are adapters. enable.<group>({ strategy, ...opts }) plumbs events to the chosen strategy.

GroupDSL optionsStrategies (shipped / planned)Default when no strategy givenOut-of-scope per memo
enable.observabilitystrategy? · sampleRate: 0..1 · detach?consoleObservability() · otelObservability(tracer) · agentcoreObservability(opts) · cloudwatchObservability(opts) · xrayObservability(opts)no-op (no auto-default)server log shipping (Splunk, etc.)
enable.coststrategy? · budget?inMemorySinkCost({onTick}) · (stripeCost — planned)inMemorySinkCost()OpenAI dashboard pipe (later)
enable.liveStatusstrategy (required) · templates?: StatusTemplateschatBubbleLiveStatus(cb)none — strategy is requiredserver-sent-events helper (later)
enable.lens (planned)(rendering target)noopLens() · (browserLens / cliLens — planned)noopLens()Lens-as-service (later)

Parked for later (same pattern, different time): audit, governance, debug, eval, reflection.

The Strategy interface

Per group, a typed interface (small, focused, no inheritance):

// As shipped — src/strategies/types.ts. Each group has its own concrete version.
interface BaseStrategy {
  readonly name: string;                       // registry key
  flush?(): void | Promise<void>;              // batch exporters
  stop?(): void;                               // teardown (idempotent)
  readonly relevantEventTypes?: readonly AgentfootprintEventType[];  // optional event filter
  validate?(): void;                           // config check at registration
  _onError?(error: Error, event?: AgentfootprintEvent): void;
}

interface ObservabilityStrategy extends BaseStrategy {
  readonly capabilities: {                     // OTel's 4-signal model
    events?: boolean;
    logs?: boolean;
    traces?: boolean;
    metrics?: boolean;
  };
  exportEvent(event: AgentfootprintEvent): void;  // hot path; sync, side-effect-only
}

Properties enforced at the interface level:

  1. Idempotent registration — registering the same name twice replaces, doesn't duplicate
  2. Side-effect-free, non-throwing hot path — the per-group hot-path method (exportEvent) must not throw, must not block. Errors caught at the dispatch layer and routed to _onError (otherwise one bad exporter kills the agent loop)
  3. Optional stop / flush — strategies that batch can flush on stop; strategies that don't need it don't implement

Same BaseStrategy shape for CostStrategy (recordCost), LiveStatusStrategy (renderStatus), LensStrategy (renderGraph) — only the hot-path method name and capability bag differ per group. Symmetry is the point.

The composite combinator

Multi-vendor fan-out comes free:

import { otelObservability } from 'agentfootprint/observability-providers';
import { composeObservability, consoleObservability } from 'agentfootprint/observability-providers';

agent.enable.observability({
  strategy: composeObservability([
    otelObservability(tracer),  // ship to your OTel collector (Datadog via OTLP, etc.)
    consoleObservability(),     // local dev visibility
  ]),
});

Shipped as of v3.1: vendor strategies live under the single grouped subpath agentfootprint/observability-providers (agentcoreObservability, cloudwatchObservability, xrayObservability, otelObservability) — not per-vendor subpaths. The default sinks (consoleObservability, inMemorySinkCost, chatBubbleLiveStatus, noopLens) and the per-group combinators (composeObservability, composeCost, composeLiveStatus, composeLens) are exported from the same place. There is no single compose — each group has its own typed combinator. Datadog ships no native adapter; point an OTel SDK at Datadog's OTLP endpoint and use otelObservability.

composeObservability returns a strategy whose exportEvent fans out to every child. Errors per-child isolated; one bad exporter doesn't break others. Same pattern as withRetry / withFallback in the resilience subsystem.

Package structure

Mirrors the existing grouped parallel-providers layout (llm-providers, tool-providers, memory-providers). One subpath per group; adding a vendor adds a factory export, not a new subpath:

agentfootprint                          ← core, ships the 4 default strategies
  /observability-providers              ← SHIPPED (v3.1): agentcore, cloudwatch, xray, otel
      peers: @aws-sdk/client-cloudwatch-logs, @aws-sdk/client-xray,
             @aws-sdk/client-bedrock-agent-runtime, @opentelemetry/api
  /cost-providers                       ← planned: stripeCost
  /lens-providers                       ← planned: browserLens, cliLens

Each vendor factory:

  • Lazy-imports its vendor SDK via lib/lazyRequire.ts (zero bundle weight when unused)
  • Peer-dep on vendor SDK (consumer brings their version)
  • peerDependenciesMeta.optional: true so missing peers don't break install

Why this matters more than "shipping integrations"

Three architectural wins:

1. Vendor swap is a one-line change

// Before
agent.enable.observability({ strategy: cloudwatchObservability({ region, logGroupName }) });

// After (vendor migration)
agent.enable.observability({ strategy: otelObservability(tracer) });

Agent code is untouched. The DSL is the contract; the vendor is the implementation. Same one-line property the cache layer offers (provider: anthropic()provider: openai()) and the memory layer (store: new InMemoryStore()store: new RedisStore()).

2. Multi-vendor fan-out without rewrites

Production observability is rarely single-vendor. Teams pipe to Datadog AND OTel AND a custom audit log AND a console. compose([...]) handles this without consumers writing fan-out code per call site.

3. CI-testable without vendor accounts

Because a strategy is just an object implementing the typed interface, CI can plug in a capture-only strategy and assert on what was exported — no vendor account or network needed:

import type { ObservabilityStrategy } from 'agentfootprint';

const captured: AgentfootprintEvent[] = [];
const captureStrategy: ObservabilityStrategy = {
  name: 'capture',
  capabilities: { events: true },
  exportEvent: (e) => { captured.push(e); },
};

agent.enable.observability({ strategy: captureStrategy });
// ... run the agent, then assert on `captured`

For cost, inMemorySinkCost() already buffers ticks for read-back. CI runs the full pipeline against the in-memory/capture strategy; production swaps in otelObservability() / cloudwatchObservability(). Mocks first, prod second — the same architectural rule as the mock() LLM provider, InMemoryStore, and mockMcpClient.

What survives the panel review (locked-in design decisions)

From the 7-expert design review (AWS IAM, Datadog, OTel, Stripe, Vercel, React, Anthropic):

#DecisionOwner
1Discriminated-union options ({ kind, ...rest }) inside each group, so new sub-features don't break the typeStripe
2Idempotent stop() — halts everything that call enabled, nothing elseStripe
3tier: 'minimal' | 'standard' | 'firehose' knob with cost-of-on docs at the call siteDatadog
4sampleRate: 0..1 on every observability enablerDatadog
5OTel exporter as the first non-default vendor (semconv alignment is free interop)OTel SIG
6mode: 'observe' | 'enforce' on audit (when it ships) — dry-run is non-negotiableAWS IAM
7Zero-arg defaults for HelloWorld (enable.observability() works without options)Vercel
8Auto-detect NODE_ENV for sensible dev/prod defaultsVercel
9compose([...]) combinator for multi-vendor fan-out + idempotent re-registrationReact
10Granular methods stay (additive, deprecated-not-removed); flat namespace marked @deprecated in JSDocStripe

Migration plan — AWS-first vendor priority

The library already has memory-agentcore (@aws-sdk/client-bedrock-agent-runtime peer dep) shipping. Established pattern. Lean into the same ecosystem for observability strategies.

Status (4.0.0): the grouped enablers (observability, cost, liveStatus), the strategy slot + the 4 default sinks (importable from agentfootprint/strategies), the per-group compose* combinators, and the AWS observability adapters (agentcoreObservability, cloudwatchObservability, xrayObservability) plus otelObservability have all shipped — under the single agentfootprint/observability-providers subpath. The deprecated flat methods enable.thinking / enable.logging were removed in 4.0.0 (migrate to enable.liveStatus / enable.observability); enable.flowchart is kept (it's a real, non-deprecated composition-graph feature). cost/lens vendor subpaths and enable.lens remain planned. The version numbers below are the original plan, kept for the historical record.

VersionWhat shipsBreaking?
v2.8.0Add the 4 grouped enablers (observability, cost, liveStatus, lens) + strategy slot + 3 default strategies (console, inMemorySink, chatBubble) + compose([...]) combinator. Granular methods kept, marked @deprecated.No
v2.8.1AWS Bedrock AgentCore Observabilityagentfootprint/observability-agentcore. Pipes events into AgentCore's native observability. Same peer (@aws-sdk/client-bedrock-agent-runtime) as memory-agentcore, so consumers already integrated with AgentCore Memory get observability for free.No
v2.8.2AWS CloudWatch Logsagentfootprint/observability-cloudwatch. Generic AWS structured logging. Peer: @aws-sdk/client-cloudwatch-logs.No
v2.8.3AWS X-Rayagentfootprint/observability-xray. Distributed tracing. Peer: @aws-sdk/client-xray.No
v2.9.xOTel exporter (observability-otel) — non-AWS but vendor-neutral; OTel SIG offered to contributeNo
v2.10.xNon-AWS observability: Datadog, PinoNo
v2.11.xcost-stripe, cost-webhookNo
v2.12.xLens strategies (browser, cliTUI, jsonExport)No
4.0.0Removed deprecated flat enable.thinking / enable.logging (migrate to enable.liveStatus / enable.observability). enable.flowchart kept — not deprecated.Breaking — single-line migration

Out of scope (parked)

These will get the same treatment but in separate design memos and future minors:

  • enable.audit — append-only, tamper-evident. Strategies: cloudtrail(), splunk(), localFile(path), noop(). Per AWS IAM review: must NOT be lumped with governance.
  • enable.governance — soft signals: budget tracking, hit-rate floors, skill-churn detection. Per AWS IAM review: separate from audit (different threat models).
  • enable.debug — Vercel-style "everything on, console output." Different defaults from observability.
  • enable.eval — trajectory scoring, golden-answer matching.
  • enable.reflection — causal-memory replay, self-improvement loops.

Each of these is well-understood structurally; they're parked because v2.8.0 should be small enough to review and ship cleanly.

Why this is a "Liskov-substitutable" architecture

Every strategy is a behavioral subtype of its interface:

ObservabilityStrategy           (the supertype — the contract)
├── consoleObservability()      (substitutable: prints structured events)
├── otelObservability(tracer)   (substitutable: forwards to OTel tracer)
├── cloudwatchObservability()   (substitutable: ships to CloudWatch Logs)
├── agentcoreObservability()    (substitutable: pipes to AgentCore)
└── composeObservability([...]) (substitutable: fans out to N children)

Agent code calls strategy.exportEvent(event) knowing only the interface. Swap any in, agent works. That's LSP enforced at runtime, not just at type-check. Same property as LLMProvider, MemoryStore, the cache strategies, and ToolProvider.

The library has one architectural pattern, applied recursively. Strategy + LSP + side-effect-import auto-registration is the spine. Cache was the first instance; v2.8 makes it the universal principle.

Pairing with other inspiration pages

The three together form the load-bearing architecture: connected data inside clean boundaries, with vendor integrations as substitutable adapters. Adapter substitutability is what keeps the data-connection guarantees from leaking across vendor boundaries.

Approval gates

Before v2.8.0 implementation starts:

  1. ✅ Design memo (this document) committed and reviewed
  2. ✅ Strategy interface signatures locked in src/strategies/types.ts
  3. ✅ Vendor adapters shipped end-to-end (AgentCore, CloudWatch, X-Ray, OTel) under observability-providers
  4. 🔲 Mock-strategy contract test — validates that ANY strategy implementing the interface plugs in cleanly
  5. 🔲 Performance baseline — compose([...]) of 5 children must add ≤ 5% overhead vs single strategy

Once gate 5 passes, we know the architecture scales linearly. Then ship the rest in independent minors.

Further reading

  • Design Patterns (Gamma et al., 1994) — Strategy + Bridge chapters
  • Cockburn, Hexagonal Architecture (2005) — port/adapter formalism
  • Plotkin & Pretnar, Handlers of Algebraic Effects (2009) — handler-stacking semantics
  • The v2.6 CHANGELOG entry — strategy pattern in our codebase, retrospective

On this page