Reference

Dependency graph (8-layer DAG)

How the library is structured. Each layer depends only on the ones above it; consumers can reach any layer directly without going through the others. The architecture diagram + the principles behind it.

A senior engineer evaluating adoption spends 5 minutes on the README, 10 minutes on examples, then wants to see the dependency graph + layer separation. This page is that map.

The 8 layers

┌─────────────────────────────────────────────────────────────┐
│ Layer 8 — Public Barrel (src/index.ts)                       │
│   What consumers `import` from. Re-exports curated subset.    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Layer 7 — Compositions (Sequence, Parallel, Conditional, Loop) │
│   Multi-runner orchestration. Each composition IS a runner.   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Layer 6 — Primitives (Agent, LLMCall)                         │
│   Single-runner units. Agent = ReAct loop. LLMCall = one shot.│
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Layer 5 — Context Engineering (InjectionEngine)               │
│   Skill, Steering, Instruction, Fact, Memory, RAG — all       │
│   reduce to {trigger, inject, flavor} via one engine.         │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Layer 4 — Memory subsystem (defineMemory + pipelines)         │
│   4 types × 7 strategies × N stores. Per-tenant isolation.    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Layer 3 — Adapters (LLM providers, MemoryStore impls, MCP)    │
│   Outer ring (Hexagonal). Hand-written shims around vendor    │
│   SDKs. Lazy-required peer-deps.                              │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Layer 2 — Event taxonomy + dispatcher                         │
│   65 typed events × 18 domains. EventDispatcher.              │
│   AgentfootprintEvent discriminated union.                    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Layer 1 — Substrate (footprintjs)                             │
│   Flowchart execution engine. DFS traversal owns the loop.    │
│   This is what makes every layer above's observability        │
│   automatic.                                                   │
└─────────────────────────────────────────────────────────────┘

Reading this diagram

  • Each layer depends only on layers below it. No cycles. A layer-5 file imports from layers 1–4 only.
  • Consumers can reach any layer directly. A power user who wants to build a custom recorder reaches Layer 2 directly via the public barrel. A power user who wants to wrap a new LLM provider implements at Layer 3 via the published interfaces.
  • The substrate at Layer 1 is what makes everything above it composable. Because footprintjs owns the traversal, every layer above gets typed-event observability for free.

Why this shape

Two properties make the architecture worth describing:

1. The substrate is shared

footprintjs at Layer 1 is the same engine running underneath Agent, Sequence, Parallel, Conditional, Loop. Every runner traverses a flowchart; every traversal emits typed events; every event flows through the same dispatcher. This is why you can agent.on('agentfootprint.composition.iteration_start') on a Loop — it's just another flowchart traversal under the hood.

2. Adapters are isolated at the outer ring

Layer 3 — providers, stores, MCP — is the only layer that talks to the outside world. The whole rest of the library has zero dependency on @anthropic-ai/sdk, openai, ioredis, @modelcontextprotocol/sdk, or any other vendor SDK. Adapters are lazy-required at first call; consumers who use only mock() + InMemoryStore install zero peer-deps. This is the Hexagonal / Ports-and-Adapters discipline (Cockburn 2005) applied honestly.

Subpath exports = subset boundaries

The package.json exports field publishes subpath imports per layer:

SubpathLayerWhat it exposes
agentfootprint (default)8The full curated barrel — 90% of consumers use only this
agentfootprint/memory4defineMemory, MEMORY_TYPES, MEMORY_STRATEGIES, InMemoryStore
agentfootprint/llm-providers3LLM provider adapters (canonical v2.5+)
agentfootprint/memory-providers3Memory store adapters — RedisStore, AgentCoreStore, future stores (canonical v2.5+)
agentfootprint/tool-providers3Tool dispatch + sources — staticTools, gatedTools, skillScopedTools, mcpClient
agentfootprint/security3Cross-cutting authorization — PermissionPolicy
agentfootprint/providers3Legacy alias for llm-providers (deprecated; still exported in v3.x — prefer llm-providers)
agentfootprint/memory-redis3Legacy per-adapter alias for memory-providers (deprecated; still exported in v3.x)
agentfootprint/memory-agentcore3Legacy per-adapter alias for memory-providers (deprecated; still exported in v3.x)
agentfootprint/observe2Observability recorders — ContextRecorder, streamRecorder, compositionRecorder, agentRecorder, boundaryRecorder, costRecorder, … (the event taxonomy + EventDispatcher are on the default barrel)
agentfootprint/resilience3withRetry, withFallback, fallbackProvider, withCircuitBreaker
agentfootprint/stream(cross-cutting)SSE bridge for streaming agent events to HTTP
agentfootprint/injection-engine5Injection, InjectionTrigger, evaluateInjections

Each subpath maps to a single layer (or in some cases a single layer's slice). Tree-shaking picks up only what's used.

The table above covers the most-reached subpaths. The full exports field also publishes agentfootprint/reliability (Layer 3 — ReliabilityFailFastError, circuit-breaker config, validators), agentfootprint/observability-providers (Layer 3 — otelObservability, cloudwatchObservability, xrayObservability, agentcoreObservability), agentfootprint/thinking, agentfootprint/status, and agentfootprint/locales (cross-cutting helpers). Check package.json's exports map for the authoritative list.

The Dynamic ReAct loop — runtime view of the same architecture

The 8-layer DAG describes the static dependency shape — what imports what. The RUNTIME shape is the agent's flowchart loop, which IS the substrate (Layer 1, footprintjs) executing through the layers above it. The loop is what makes the architecture EXPRESSIVE.

                                ┌──────────────────────────┐
                                │  agent.run({message})    │
                                └──────────┬───────────────┘

        ┌──────────────────────────────────────────────────────────────────┐
        │  iteration N starts here every time the loop iterates             │
        │                                                                   │
        │   ┌──────────────────┐                                            │
        │   │ INJECTION_ENGINE │  evaluates EVERY trigger this iteration   │
        │   └──────────┬───────┘  (always / rule / on-tool-return /         │
        │              ▼            llm-activated). Outputs activeInjections │
        │   ┌──────────────────┐                                            │
        │   │  SYSTEM_PROMPT   │  composes system slot from active set      │
        │   └──────────┬───────┘                                            │
        │              ▼                                                    │
        │   ┌──────────────────┐                                            │
        │   │     MESSAGES     │  composes messages slot (+ history)        │
        │   └──────────┬───────┘                                            │
        │              ▼                                                    │
        │   ┌──────────────────┐                                            │
        │   │      TOOLS       │  composes tools slot (per-skill gating,    │
        │   └──────────┬───────┘   permission filter — Block A1+A2+A5)      │
        │              ▼                                                    │
        │   ┌──────────────────┐                                            │
        │   │     CallLLM      │  the LLM sees a freshly-composed           │
        │   └──────────┬───────┘   prompt + tool list every iteration       │
        │              ▼                                                    │
        │   ┌──────────────────┐                                            │
        │   │   Route decider  │  tool calls? → execute → loop back ↑       │
        │   └─────┬────────────┘  no tool calls? → final → done             │
        │         │                                                         │
        └─────────┼─────────────────────────────────────────────────────────┘
                  │ if tool calls executed:
                  └─► append result to history → loopTo(INJECTION_ENGINE)

                                  RE-RUNS THE WHOLE PIPELINE TOP-DOWN
                                  (this is what makes Dynamic ReAct work)

Why this matters architecturally: every iteration re-evaluates triggers against the FRESHEST context. Tool results from iteration N reshape the system prompt, tool list, and active skills for iteration N+1. The framework's typed-event stream + replayable traces are the OBSERVABILITY consequence; per-iteration recomposition is the EXPRESSIVE consequence.

This is what we mean by "the framework owns the loop." It's not just that we own the iteration count — we own the slot composition pipeline that runs THROUGH each iteration. Without that, "context engineering" collapses to "static prompt assembly" and the framework's distinctness disappears.

See Dynamic ReAct guide for the full mechanics + use cases.

What this enables

  • Power users build at the layer they need. Want a custom Memory store? Implement Layer 3's MemoryStore interface; everything above unchanged. Want a new pattern? Compose Layer 7 over existing Layer 6 primitives.
  • Plugin authors build at the boundary. A "Skills inspector" library subscribes to agentfootprint.skill.* events at Layer 2; doesn't need to touch the InjectionEngine.
  • The library's own internals follow the same rule. No layer-7 file imports a layer-6 file's private internals; layer-6 files import only published Layer-5 + below interfaces.

Anti-cycles enforced by tooling

scripts/check-dup-types.mjs runs in CI to detect circular type imports across layers. The release script (Gate 2.5) blocks releases that introduce cycles.

  • GoF Adapter pattern — every Layer-3 file is one
  • GoF Decorator patternwithRetry / withFallback (Layer 3, on top of LLMProvider)
  • Hexagonal Architecture (Cockburn 2005) — Layers 1–2 are the inner rings; Layer 3 is the outer ring
  • footprintjs flowchart pattern — the substrate that makes the typed-event observability automatic

Next steps

  • Manifesto — the framework's perspective on what it is + isn't
  • Custom provider — implement at Layer 3
  • footprintjs — Layer 1 substrate, if you want to build at this depth

On this page