Skip to content

Dependency graph (8-layer DAG)

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.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 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, slot, content} 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 β”‚
β”‚ 47 typed events Γ— 13 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. β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • 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.

Two properties make the architecture worth describing:

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.

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.

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 (still works through v2.x; removed v3.0)
agentfootprint/memory-redis3Legacy per-adapter alias (still works through v2.x; removed v3.0)
agentfootprint/memory-agentcore3Legacy per-adapter alias (still works through v2.x; removed v3.0)
agentfootprint/observe2Event taxonomy + dispatcher types
agentfootprint/resilience3withRetry, withFallback, resilientProvider
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 Dynamic ReAct loop β€” runtime view of the same architecture

Section titled β€œ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.

  • 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.

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 pattern β€” withRetry / 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
  • 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