agentfootprint 9.114.2 → 9.115.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/AGENTS.md +205 -528
  2. package/CAPABILITIES.md +3 -0
  3. package/README.md +33 -790
  4. package/ai-instructions/claude-code/SKILL.md +26 -3
  5. package/bin/agentfootprint-lint-tools.mjs +1 -1
  6. package/dist/adapters/hosting/agentCoreA2A.js +5 -0
  7. package/dist/adapters/hosting/agentCoreA2A.js.map +1 -1
  8. package/dist/adapters/hosting/agentcore.js +5 -0
  9. package/dist/adapters/hosting/agentcore.js.map +1 -1
  10. package/dist/adapters/hosting/foundryResponses.js +5 -0
  11. package/dist/adapters/hosting/foundryResponses.js.map +1 -1
  12. package/dist/adapters/observability/agentcore.js +18 -5
  13. package/dist/adapters/observability/agentcore.js.map +1 -1
  14. package/dist/adapters/observability/audit.js +17 -4
  15. package/dist/adapters/observability/audit.js.map +1 -1
  16. package/dist/adapters/observability/otel.js +890 -33
  17. package/dist/adapters/observability/otel.js.map +1 -1
  18. package/dist/artifacts/capability.js +9 -3
  19. package/dist/artifacts/capability.js.map +1 -1
  20. package/dist/bridge/eventMeta.js +40 -1
  21. package/dist/bridge/eventMeta.js.map +1 -1
  22. package/dist/core/Agent.js +40 -6
  23. package/dist/core/Agent.js.map +1 -1
  24. package/dist/core/RunnerBase.js +32 -1
  25. package/dist/core/RunnerBase.js.map +1 -1
  26. package/dist/core/agent/AgentBuilder.js +13 -1
  27. package/dist/core/agent/AgentBuilder.js.map +1 -1
  28. package/dist/core/agent/stages/toolCalls.js +64 -7
  29. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  30. package/dist/core/flowchartAsTool.js +8 -2
  31. package/dist/core/flowchartAsTool.js.map +1 -1
  32. package/dist/core/runbook/runbookAsTool.js +7 -2
  33. package/dist/core/runbook/runbookAsTool.js.map +1 -1
  34. package/dist/core/toolShownArgs.js +55 -0
  35. package/dist/core/toolShownArgs.js.map +1 -0
  36. package/dist/debug.js +1 -1
  37. package/dist/debug.js.map +1 -1
  38. package/dist/esm/adapters/hosting/agentCoreA2A.d.ts +14 -1
  39. package/dist/esm/adapters/hosting/agentCoreA2A.js +5 -0
  40. package/dist/esm/adapters/hosting/agentCoreA2A.js.map +1 -1
  41. package/dist/esm/adapters/hosting/agentcore.d.ts +18 -2
  42. package/dist/esm/adapters/hosting/agentcore.js +5 -0
  43. package/dist/esm/adapters/hosting/agentcore.js.map +1 -1
  44. package/dist/esm/adapters/hosting/foundryResponses.d.ts +15 -1
  45. package/dist/esm/adapters/hosting/foundryResponses.js +5 -0
  46. package/dist/esm/adapters/hosting/foundryResponses.js.map +1 -1
  47. package/dist/esm/adapters/observability/agentcore.d.ts +17 -5
  48. package/dist/esm/adapters/observability/agentcore.js +18 -5
  49. package/dist/esm/adapters/observability/agentcore.js.map +1 -1
  50. package/dist/esm/adapters/observability/audit.d.ts +6 -2
  51. package/dist/esm/adapters/observability/audit.js +17 -4
  52. package/dist/esm/adapters/observability/audit.js.map +1 -1
  53. package/dist/esm/adapters/observability/otel.d.ts +240 -11
  54. package/dist/esm/adapters/observability/otel.js +890 -33
  55. package/dist/esm/adapters/observability/otel.js.map +1 -1
  56. package/dist/esm/artifacts/capability.d.ts +3 -1
  57. package/dist/esm/artifacts/capability.js +9 -3
  58. package/dist/esm/artifacts/capability.js.map +1 -1
  59. package/dist/esm/bridge/eventMeta.d.ts +32 -0
  60. package/dist/esm/bridge/eventMeta.js +38 -0
  61. package/dist/esm/bridge/eventMeta.js.map +1 -1
  62. package/dist/esm/core/Agent.d.ts +18 -5
  63. package/dist/esm/core/Agent.js +42 -8
  64. package/dist/esm/core/Agent.js.map +1 -1
  65. package/dist/esm/core/RunnerBase.d.ts +20 -0
  66. package/dist/esm/core/RunnerBase.js +32 -1
  67. package/dist/esm/core/RunnerBase.js.map +1 -1
  68. package/dist/esm/core/agent/AgentBuilder.js +13 -1
  69. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  70. package/dist/esm/core/agent/stages/toolCalls.d.ts +11 -0
  71. package/dist/esm/core/agent/stages/toolCalls.js +64 -7
  72. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  73. package/dist/esm/core/flowchartAsTool.js +8 -2
  74. package/dist/esm/core/flowchartAsTool.js.map +1 -1
  75. package/dist/esm/core/runbook/runbookAsTool.js +7 -2
  76. package/dist/esm/core/runbook/runbookAsTool.js.map +1 -1
  77. package/dist/esm/core/toolShownArgs.d.ts +62 -0
  78. package/dist/esm/core/toolShownArgs.js +49 -0
  79. package/dist/esm/core/toolShownArgs.js.map +1 -0
  80. package/dist/esm/core-flow/Parallel.d.ts +1 -1
  81. package/dist/esm/debug.js +1 -1
  82. package/dist/esm/debug.js.map +1 -1
  83. package/dist/esm/events/payloads.d.ts +122 -6
  84. package/dist/esm/events/registry.d.ts +3 -1
  85. package/dist/esm/events/registry.js +2 -0
  86. package/dist/esm/events/registry.js.map +1 -1
  87. package/dist/esm/events/types.d.ts +7 -0
  88. package/dist/esm/hosting/artifactWire.d.ts +10 -2
  89. package/dist/esm/hosting/artifactWire.js +20 -2
  90. package/dist/esm/hosting/artifactWire.js.map +1 -1
  91. package/dist/esm/hosting/doorGuard.d.ts +280 -0
  92. package/dist/esm/hosting/doorGuard.js +499 -0
  93. package/dist/esm/hosting/doorGuard.js.map +1 -0
  94. package/dist/esm/hosting/errors.d.ts +113 -0
  95. package/dist/esm/hosting/errors.js +177 -0
  96. package/dist/esm/hosting/errors.js.map +1 -1
  97. package/dist/esm/hosting/httpHost.d.ts +32 -3
  98. package/dist/esm/hosting/httpHost.js +218 -4
  99. package/dist/esm/hosting/httpHost.js.map +1 -1
  100. package/dist/esm/hosting/index.d.ts +7 -5
  101. package/dist/esm/hosting/index.js +10 -3
  102. package/dist/esm/hosting/index.js.map +1 -1
  103. package/dist/esm/hosting/ingressRecord.d.ts +64 -10
  104. package/dist/esm/hosting/ingressRecord.js +65 -6
  105. package/dist/esm/hosting/ingressRecord.js.map +1 -1
  106. package/dist/esm/hosting/nodeHost.d.ts +32 -2
  107. package/dist/esm/hosting/nodeHost.js +10 -0
  108. package/dist/esm/hosting/nodeHost.js.map +1 -1
  109. package/dist/esm/hosting/standingAgent.js +362 -51
  110. package/dist/esm/hosting/standingAgent.js.map +1 -1
  111. package/dist/esm/hosting/turnArtifacts.d.ts +93 -0
  112. package/dist/esm/hosting/turnArtifacts.js +126 -0
  113. package/dist/esm/hosting/turnArtifacts.js.map +1 -0
  114. package/dist/esm/hosting/types.d.ts +196 -3
  115. package/dist/esm/hosting/types.js +10 -0
  116. package/dist/esm/hosting/types.js.map +1 -1
  117. package/dist/esm/hosting/webSocketConversation.d.ts +15 -1
  118. package/dist/esm/hosting/webSocketConversation.js +54 -2
  119. package/dist/esm/hosting/webSocketConversation.js.map +1 -1
  120. package/dist/esm/integrity/disposition/ledger.d.ts +8 -0
  121. package/dist/esm/integrity/disposition/ledger.js +0 -0
  122. package/dist/esm/integrity/disposition/ledger.js.map +1 -1
  123. package/dist/esm/lib/mcp/mcpServe.js +39 -2
  124. package/dist/esm/lib/mcp/mcpServe.js.map +1 -1
  125. package/dist/esm/lib/mcp/types.d.ts +30 -0
  126. package/dist/esm/lib/tool-lint/index.d.ts +1 -1
  127. package/dist/esm/lib/tool-lint/index.js +1 -1
  128. package/dist/esm/recorders/observability/commentary/artifactPhrases.d.ts +4 -0
  129. package/dist/esm/recorders/observability/commentary/artifactPhrases.js +9 -0
  130. package/dist/esm/recorders/observability/commentary/artifactPhrases.js.map +1 -1
  131. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js +30 -3
  132. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  133. package/dist/esm/recorders/observability/recordRun.d.ts +12 -0
  134. package/dist/esm/recorders/observability/recordRun.js +17 -1
  135. package/dist/esm/recorders/observability/recordRun.js.map +1 -1
  136. package/dist/esm/strategies/attach.js +87 -1
  137. package/dist/esm/strategies/attach.js.map +1 -1
  138. package/dist/events/registry.js +2 -0
  139. package/dist/events/registry.js.map +1 -1
  140. package/dist/hosting/artifactWire.js +21 -3
  141. package/dist/hosting/artifactWire.js.map +1 -1
  142. package/dist/hosting/doorGuard.js +509 -0
  143. package/dist/hosting/doorGuard.js.map +1 -0
  144. package/dist/hosting/errors.js +183 -1
  145. package/dist/hosting/errors.js.map +1 -1
  146. package/dist/hosting/httpHost.js +218 -4
  147. package/dist/hosting/httpHost.js.map +1 -1
  148. package/dist/hosting/index.js +19 -2
  149. package/dist/hosting/index.js.map +1 -1
  150. package/dist/hosting/ingressRecord.js +66 -6
  151. package/dist/hosting/ingressRecord.js.map +1 -1
  152. package/dist/hosting/nodeHost.js +11 -1
  153. package/dist/hosting/nodeHost.js.map +1 -1
  154. package/dist/hosting/standingAgent.js +359 -48
  155. package/dist/hosting/standingAgent.js.map +1 -1
  156. package/dist/hosting/turnArtifacts.js +130 -0
  157. package/dist/hosting/turnArtifacts.js.map +1 -0
  158. package/dist/hosting/types.js +11 -1
  159. package/dist/hosting/types.js.map +1 -1
  160. package/dist/hosting/webSocketConversation.js +54 -2
  161. package/dist/hosting/webSocketConversation.js.map +1 -1
  162. package/dist/integrity/disposition/ledger.js +0 -0
  163. package/dist/integrity/disposition/ledger.js.map +1 -1
  164. package/dist/lib/mcp/mcpServe.js +39 -2
  165. package/dist/lib/mcp/mcpServe.js.map +1 -1
  166. package/dist/lib/tool-lint/index.js +1 -1
  167. package/dist/recorders/observability/commentary/artifactPhrases.js +10 -1
  168. package/dist/recorders/observability/commentary/artifactPhrases.js.map +1 -1
  169. package/dist/recorders/observability/commentary/commentaryTemplates.js +29 -2
  170. package/dist/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  171. package/dist/recorders/observability/recordRun.js +20 -3
  172. package/dist/recorders/observability/recordRun.js.map +1 -1
  173. package/dist/strategies/attach.js +87 -1
  174. package/dist/strategies/attach.js.map +1 -1
  175. package/dist/types/adapters/hosting/agentCoreA2A.d.ts +14 -1
  176. package/dist/types/adapters/hosting/agentCoreA2A.d.ts.map +1 -1
  177. package/dist/types/adapters/hosting/agentcore.d.ts +18 -2
  178. package/dist/types/adapters/hosting/agentcore.d.ts.map +1 -1
  179. package/dist/types/adapters/hosting/foundryResponses.d.ts +15 -1
  180. package/dist/types/adapters/hosting/foundryResponses.d.ts.map +1 -1
  181. package/dist/types/adapters/observability/agentcore.d.ts +17 -5
  182. package/dist/types/adapters/observability/agentcore.d.ts.map +1 -1
  183. package/dist/types/adapters/observability/audit.d.ts +6 -2
  184. package/dist/types/adapters/observability/audit.d.ts.map +1 -1
  185. package/dist/types/adapters/observability/otel.d.ts +240 -11
  186. package/dist/types/adapters/observability/otel.d.ts.map +1 -1
  187. package/dist/types/artifacts/capability.d.ts +3 -1
  188. package/dist/types/artifacts/capability.d.ts.map +1 -1
  189. package/dist/types/bridge/eventMeta.d.ts +32 -0
  190. package/dist/types/bridge/eventMeta.d.ts.map +1 -1
  191. package/dist/types/core/Agent.d.ts +18 -5
  192. package/dist/types/core/Agent.d.ts.map +1 -1
  193. package/dist/types/core/RunnerBase.d.ts +20 -0
  194. package/dist/types/core/RunnerBase.d.ts.map +1 -1
  195. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  196. package/dist/types/core/agent/stages/toolCalls.d.ts +11 -0
  197. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  198. package/dist/types/core/flowchartAsTool.d.ts.map +1 -1
  199. package/dist/types/core/runbook/runbookAsTool.d.ts.map +1 -1
  200. package/dist/types/core/toolShownArgs.d.ts +63 -0
  201. package/dist/types/core/toolShownArgs.d.ts.map +1 -0
  202. package/dist/types/core-flow/Parallel.d.ts +1 -1
  203. package/dist/types/events/payloads.d.ts +122 -6
  204. package/dist/types/events/payloads.d.ts.map +1 -1
  205. package/dist/types/events/registry.d.ts +3 -1
  206. package/dist/types/events/registry.d.ts.map +1 -1
  207. package/dist/types/events/types.d.ts +7 -0
  208. package/dist/types/events/types.d.ts.map +1 -1
  209. package/dist/types/hosting/artifactWire.d.ts +10 -2
  210. package/dist/types/hosting/artifactWire.d.ts.map +1 -1
  211. package/dist/types/hosting/doorGuard.d.ts +281 -0
  212. package/dist/types/hosting/doorGuard.d.ts.map +1 -0
  213. package/dist/types/hosting/errors.d.ts +113 -0
  214. package/dist/types/hosting/errors.d.ts.map +1 -1
  215. package/dist/types/hosting/httpHost.d.ts +32 -3
  216. package/dist/types/hosting/httpHost.d.ts.map +1 -1
  217. package/dist/types/hosting/index.d.ts +7 -5
  218. package/dist/types/hosting/index.d.ts.map +1 -1
  219. package/dist/types/hosting/ingressRecord.d.ts +64 -10
  220. package/dist/types/hosting/ingressRecord.d.ts.map +1 -1
  221. package/dist/types/hosting/nodeHost.d.ts +32 -2
  222. package/dist/types/hosting/nodeHost.d.ts.map +1 -1
  223. package/dist/types/hosting/standingAgent.d.ts.map +1 -1
  224. package/dist/types/hosting/turnArtifacts.d.ts +94 -0
  225. package/dist/types/hosting/turnArtifacts.d.ts.map +1 -0
  226. package/dist/types/hosting/types.d.ts +196 -3
  227. package/dist/types/hosting/types.d.ts.map +1 -1
  228. package/dist/types/hosting/webSocketConversation.d.ts +15 -1
  229. package/dist/types/hosting/webSocketConversation.d.ts.map +1 -1
  230. package/dist/types/integrity/disposition/ledger.d.ts +8 -0
  231. package/dist/types/integrity/disposition/ledger.d.ts.map +1 -1
  232. package/dist/types/lib/mcp/mcpServe.d.ts.map +1 -1
  233. package/dist/types/lib/mcp/types.d.ts +30 -0
  234. package/dist/types/lib/mcp/types.d.ts.map +1 -1
  235. package/dist/types/lib/tool-lint/index.d.ts +1 -1
  236. package/dist/types/recorders/observability/commentary/artifactPhrases.d.ts +4 -0
  237. package/dist/types/recorders/observability/commentary/artifactPhrases.d.ts.map +1 -1
  238. package/dist/types/recorders/observability/commentary/commentaryTemplates.d.ts.map +1 -1
  239. package/dist/types/recorders/observability/recordRun.d.ts +12 -0
  240. package/dist/types/recorders/observability/recordRun.d.ts.map +1 -1
  241. package/dist/types/strategies/attach.d.ts.map +1 -1
  242. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -1,602 +1,288 @@
1
1
  # agentfootprint — Agent Instructions (OpenAI Codex)
2
2
 
3
- This is the **agentfootprint** library — a framework for building Generative AI applications where context engineering is buildable at the control-flow level. Built on [footprintjs](https://github.com/footprintjs/footPrint) (the flowchart pattern for backend code).
3
+ > GENERATED from `ai-instructions/claude-code/SKILL.md` by `npm run docs:regen` — edit that file.
4
+ > Search [CAPABILITIES.md](https://github.com/footprintjs/agentfootprint/blob/main/CAPABILITIES.md) before building
5
+ > anything — it may already ship. Contributing to agentfootprint itself? Read its
6
+ > [CLAUDE.md](https://github.com/footprintjs/agentfootprint/blob/main/CLAUDE.md) (the definition of done).
4
7
 
5
- ## Core Thesis
8
+ agentfootprint structures AI agents as composable flowcharts, so every injection, read, write, decision and tool call becomes connected evidence as the run happens. Every concept takes an `LLMProvider` — swap `mock({...})` for `anthropic({...})` with zero code changes.
6
9
 
7
- **Building Generative AI applications is mostly *context engineering*** — deciding what content lands in which slot of the LLM call, when, and why. agentfootprint exposes this discipline through:
10
+ **Core principles:**
11
+ - Adapter-swap testing ($0 test runs, deterministic assertions)
12
+ - The ladder: `mock` → `ollama` (free, local, real model) → a paid provider
13
+ - Declare context (facts, steering, skills); the framework decides WHEN it fires and WHICH slot it lands in
14
+ - Collect during traversal, never post-process (inherited from footprintjs)
8
15
 
9
- - **2 primitives** — `LLMCall`, `Agent` (= ReAct loop)
10
- - **3 compositions + Loop** — `Sequence` · `Parallel` · `Conditional` · `Loop`
11
- - **1 unifying injection primitive** — `Injection` with 4 typed sugar factories
12
- - **1 memory factory** — `defineMemory({ type, strategy, store })`
13
-
14
- Every named pattern (Reflexion, ToT, Swarm, ...) is a recipe over these. **Don't ship new classes per paper.**
16
+ ```bash
17
+ npm install agentfootprint footprintjs
18
+ ```
15
19
 
16
- ## The Mental Model — Three Slots, Six Flavors
20
+ ## Read this first — what does NOT exist
17
21
 
18
- Every LLM call has three slots. Every "agent feature" is content flowing into one of them:
22
+ These are not hypothetical. A capable author, working from a correct mental model of
23
+ this library, invented all four in one document. Treat them as the things you are
24
+ most likely to get wrong here.
19
25
 
20
- | LLM API field | What goes here |
26
+ | You will reach for | The reality |
21
27
  |---|---|
22
- | `system` prompt | Steering · Instruction text · Skill body · Fact data · formatted memory |
23
- | `messages` array | The conversation — user turns, assistant turns, tool results. Assembled from the conversation itself, never injected into |
24
- | `tools` array | Tool schemas (registered + Skill-attached) |
28
+ | `startRun(...)` | **No such function.** The door is `agent.run(input, options?)`, where `AgentInput = { message: string; identity?; continueFrom? }` and `AgentOutput = string`. `run()` returns `AgentOutput \| RunnerPauseOutcome` — a run paused for a human returns a checkpoint; discriminate with `isPaused(result)`. |
29
+ | `RunStep` as skill/route history | **`RunStep` is real and it is something else** — the footprintjs flowchart TOPOLOGY slider, exported from `agentfootprint/observe`. Its `kind` is `'sequential' \| 'fork' \| 'merge' \| 'decide' \| 'iteration' \| 'iteration-exit' \| 'react'`. Nothing in it concerns skills. Importing it succeeds, which is exactly why it is dangerous. For route history use `routeRecorder()` from the same door. |
30
+ | the LLM classifier as routing "tier 3" | **It is a tier-2 strategy.** Tier 1 = declared start rules. Tier 2 = the configured scorer — `llmClassifier(provider)` OR `keywordScorer()` OR `embeddingScorer(e)` OR the entry scorer; near-ties fall through rather than argmax. Tier 3 = a menu the model resolves in-band through `read_skill`'s own description, reached only when tier 2 was NOT decisive. |
31
+ | a skill's tools being gated to that skill automatically | **They are not, by default.** `defineSkill({ tools })` puts them in the agent's static tool list at build time — visible from iteration 1 whether the skill ever activates or not. Ask for the gate: `.toolsFromActiveSkill()` (agent-wide), `skillGraph({ scopeTools: true })` (graph-wide), or `autoActivate: 'currentSkill'` (per skill). `.tree()` leaves are the one shape scoped by default. |
32
+
33
+ Two more absences: there is **no runtime force-stop governor** (`routeRecorder().getTrips()`
34
+ only *labels* a spinning run; `maxIterations` is the hard stop), and there is **no
35
+ automatic re-delivery of an ageing skill body** (`refreshPolicy` is stored and never
36
+ read on any version — use `surfaceMode: 'both'`).
25
37
 
26
- The flavors are how you *mark intent* — but they all reduce to one `Injection` primitive:
38
+ ## Subpath map — 17 doors
27
39
 
28
- | Flavor | Trigger | Slots |
29
- |---|---|---|
30
- | **Skill** | LLM-activated (`read_skill`) | system-prompt + tools |
31
- | **Steering** | Always-on | system-prompt |
32
- | **Instruction** | Predicate (`activeWhen` / `on-tool-return`) | system-prompt |
33
- | **Fact** | Always-on (data) | system-prompt |
40
+ `agentfootprint` (main barrel: `Agent`, `LLMCall`, `defineTool`, control flow, patterns, `defineRAG`, pause/resume) · `/providers` (`mock`, `anthropic`, `openai`, `bedrock`, `ollama`, `mcpClient`, embedders — every provider, so bundlers never walk the vendor SDKs from the main barrel) · `/context` (`defineSkill`, `defineFact`, `defineSteering`, `defineInstruction`, `skillGraph`, `skillsFromDir`, the scorers) · `/memory` (`defineMemory`, `InMemoryStore`, `mockEmbedder`, the stores) · `/rag` (stores + loaders; `defineRAG` itself is on the main barrel) · `/observe` (recorders, tracing, `RunStep`) · `/resilience` (provider decorators) · `/reliability` (the rules-based fail-fast gate) · `/cache` (prefix-cache strategies; importing it registers them) · `/security` · `/hosting` · `/events` · `/skill-graph` (the routing layer with no framework attached, for a host that is not this agent) · `/recipes` (`defineAgentRecipe` — one named, versioned agent setup, applied with `.recipe()`) · `/maps` (`Claim<T>` and the map-engagement vocabulary behind `.maps()`) · `/classify` (the calibrated-classifier port: `Classifier`, `mockClassifier`) · `/ontology` (`defineOntology` — a declared map of the domain, mounted with `.ontology()`). `package.json` `exports` is the complete list; a path not on it does not resolve.
34
41
 
35
- ## Mock-first development (RECOMMENDED workflow)
42
+ ## Core Concepts
36
43
 
37
- Build the entire app — agent, context engineering, tools, memory, RAG, MCP — against in-memory mocks first. Validate logic and patterns end-to-end with $0 API cost. Swap real infrastructure in, one boundary at a time, only after the flow is right.
44
+ ### LLMCall — a single LLM call, no tools
38
45
 
39
46
  ```typescript
40
- import { Agent, defineTool } from 'agentfootprint';
47
+ import { LLMCall } from 'agentfootprint';
41
48
  import { mock } from 'agentfootprint/providers';
42
- import { InMemoryStore, mockEmbedder } from 'agentfootprint/memory';
43
-
44
- // Mock LLM provider — no API key, deterministic reply.
45
- const provider = mock({ reply: 'Refunds take 3 business days.' });
46
49
 
47
- // Inline-mocked tool — no real backend yet.
48
- const lookup = defineTool({
49
- name: 'lookup',
50
- description: '...',
51
- inputSchema: { type: 'object', properties: {} },
52
- execute: async () => 'mock data',
53
- });
54
-
55
- const agent = Agent.create({ provider, model: 'mock' }).tool(lookup).build();
56
- await agent.run({ message: 'How long does a refund take?' });
50
+ const caller = LLMCall.create({ provider: mock({ reply: 'Hello!' }), model: 'mock' }).system('You are helpful.').build();
51
+ const result = await caller.run({ message: 'Hi' });
57
52
  ```
58
53
 
59
- | Boundary | Mock for development | Production swap |
60
- |---|---|---|
61
- | LLM provider | `mock({ reply })` · `mock({ replies })` for scripted ReAct | `ollama('<model>')` — a real model, still $0 and no key · then `anthropic()` · `openai()` · `bedrock()` |
62
- | Embedder | `mockEmbedder()` | OpenAI / Cohere / Bedrock embedder factory |
63
- | Memory store | `InMemoryStore` | `RedisStore` (`agentfootprint/memory`) · `AgentCoreStore` (`agentfootprint/memory`) · DynamoDB / Postgres / Pinecone (planned) |
64
- | MCP server | `mockMcpClient({ tools })` — in-memory, no SDK | `mcpClient({ transport })` to a real server |
65
- | Tool execute | inline `async () => '...'` closure | real implementation |
66
-
67
- When generating starter code for users, **default to the mock surface** unless they explicitly say they have a key / endpoint / store ready. Show real-provider code as the "swap" step, not the first step.
68
-
69
- **Subpath imports** for memory adapters keep the main barrel small + tree-shaking clean:
54
+ ### Agent — a ReAct agent with tools
70
55
 
71
56
  ```typescript
72
- import { RedisStore } from 'agentfootprint/memory';
73
- import { AgentCoreStore } from 'agentfootprint/memory';
74
- ```
75
-
76
- Both lazy-require their SDK (`ioredis` / `@aws-sdk/client-bedrock-agent-runtime`) and accept `_client` for test injection.
77
-
78
- **Multi-turn mock for tool-using ReAct:**
57
+ import { Agent, defineTool } from 'agentfootprint';
58
+ import { mock } from 'agentfootprint/providers';
79
59
 
80
- ```typescript
81
- const provider = mock({
82
- replies: [
83
- { toolCalls: [{ id: '1', name: 'lookup', args: { topic: 'refunds' } }] },
84
- { content: 'Refunds take 3 business days.' },
85
- ],
60
+ const weather = defineTool({
61
+ name: 'weather',
62
+ description: 'Get current weather for a city.',
63
+ inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
64
+ execute: async ({ city }: { city: string }) => `${city}: 72°F, sunny`,
86
65
  });
87
- ```
88
66
 
89
- Each `complete()` consumes one reply in order. Exhaustion throws loud — misnumbered scripts fail tests instead of silently looping.
67
+ const agent = Agent.create({ provider: mock({ reply: 'It is 72°F.' }), model: 'mock' })
68
+ .system('You answer weather questions using the weather tool.')
69
+ .tool(weather)
70
+ .maxIterations(5)
71
+ .build();
90
72
 
91
- ## Public API
73
+ const result = await agent.run({ message: 'Weather in Paris?' });
74
+ ```
92
75
 
93
- ### MCP — `mcpClient` (connect to MCP servers, register their tools)
76
+ ### Context — facts, steering, skills, and declared routing
94
77
 
95
78
  ```typescript
96
- import { Agent, mcpClient } from 'agentfootprint';
79
+ import { defineFact, defineSteering, defineSkill, skillGraph } from 'agentfootprint/context';
97
80
 
98
- const slack = await mcpClient({
99
- name: 'slack',
100
- transport: { transport: 'stdio', command: 'npx', args: ['@example/slack-mcp'] },
101
- });
102
-
103
- const agent = Agent.create({ provider })
104
- .tools(await slack.tools()) // pull ALL tools from the server in one call
81
+ Agent.create({ provider, model })
82
+ .fact(defineFact({ id: 'user-profile', data: 'Plan: Pro · Customer since 2022' }))
83
+ .steering(defineSteering({ id: 'policy', prompt: 'Never promise a refund before checking.' }))
84
+ .skill(defineSkill({ id: 'refunds', description: 'Refund procedure.', body: '…', tools: [issueRefund] }))
105
85
  .build();
106
-
107
- await agent.run({ message: '...' });
108
- await slack.close();
109
86
  ```
110
87
 
111
- Transports: `stdio` (local subprocess), `http` (Streamable HTTP). The
112
- `@modelcontextprotocol/sdk` peer-dep is lazy-required — zero runtime
113
- cost when MCP isn't used. Friendly install hint if missing.
114
-
115
- **In a BROWSER** the Node `require` loader does not exist, so supply what it
116
- would have found. Give the two SDK modules as `sdk` (the library still builds
117
- the transport, so headers / your own `fetch` / gateway vending / throttle retry
118
- all keep working), or a client you connected yourself as `connection`:
88
+ `defineSkill` bodies load on demand — the model opens one with `read_skill`, or a `skillGraph()` routes to it:
119
89
 
120
90
  ```typescript
121
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
122
- import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
91
+ const graph = skillGraph()
92
+ .entry(triage, { when: (c) => /order/.test(c.userMessage) }) // where the turn STARTS
93
+ .route(triage, refunds, { onToolReturn: 'lookup_order' }) // a declared handoff
94
+ .build();
123
95
 
124
- const sidecar = await mcpClient({
125
- name: 'sidecar',
126
- sdk: { Client, StreamableHTTPClientTransport },
127
- transport: { transport: 'http', url: '/py/mcp' }, // relative resolves against the page
128
- });
96
+ Agent.create({ provider, model }).skillGraph(graph).build();
97
+ graph.toMermaid(); // declared === drawn
129
98
  ```
130
99
 
131
- `stdio` spawns a subprocess and is refused in words there. On the `connection`
132
- arm the library builds no transport, so `retryOnThrottle` / `clientInfo` are
133
- REFUSED rather than ignored — wrap your own fetch with `retryingFetch` instead.
100
+ `.entry()` and `.route()` take the skill OBJECTS, not their ids. The object form is
101
+ the other door — `skillGraph({ skills, start, steps })` returns a finished graph with
102
+ nothing to chain.
103
+
104
+ A skill is active exactly while the cursor is on it — one skill's turn at a time. An `.entry(x)` with **no** `when` is the persistent base (`always`), on beside whatever the cursor is on.
105
+
106
+ **The cursor is a program counter, not a per-turn classifier.** Nine causes move it (or
107
+ decline to), reported as `cursorMove.by` on `agentfootprint.context.evaluated` and as
108
+ `outcome` on `routeRecorder().getHops()`:
109
+
110
+ `'entry'` (cold start) · `'route'` (a declared `from`-gated edge fired) · `'tool-proposal'`
111
+ (a TOOL RESULT proposed a transition and the graph accepted it) · `'model-pick'` (a
112
+ gate-accepted `read_skill`) · `'intent'` (the tier-2 scorer was decisive) · `'continuity'`
113
+ (the cursor inherited from the previous turn held) · `'decider'` (an out-of-band menu
114
+ resolver) · `'stay'` (nothing fired — sticky, and a recorded decision, not an absence) ·
115
+ `'none'` (no cursor at all: nothing to enter, or a `tree()`, which has no cursor).
116
+ `routeRecorder`'s `RouteOutcome` is those eight minus `'none'` (no cursor, no hop) plus
117
+ `'rejected'` — nine values. Precedence when several want it at once:
118
+ **declared edge > accepted tool proposal > model pick > stay.** A suppressed pick emits
119
+ `agentfootprint.skill.reroute_superseded`; a parallel batch matching different targets
120
+ emits `agentfootprint.skill.route_conflict`.
121
+
122
+ **The cursor is per RUN by default.** A second `run()` starts cold at the entry.
123
+ `.skillGraph(graph, { continuity: 'conversation' })` makes it span the conversation.
124
+
125
+ **`read_skill` has a three-way design, not one list.** Per iteration a skill is
126
+ *reachable* (named under "Reachable from here"), *refusable* (named under "Not reachable
127
+ from here" — a graph refusal is about WHERE THE CURSOR IS, so naming it lets the model
128
+ route in one step), or *hidden* (absent entirely — a hidden skill is about WHO IS ASKING,
129
+ and naming it would leak the shape of somebody else's permissions; needs a
130
+ `PermissionChecker` governing `skill_read`). **The enum stays the full catalog in every
131
+ case** — narrowing it would turn a policy refusal into a generic schema error the model
132
+ never reads.
133
+
134
+ A refused pick gets one teaching sentence back and moves nothing:
135
+
136
+ ```text
137
+ read_skill("audit-log") is not reachable from here. Reachable skills: billing. Pick one of these, or finish.
138
+ ```
134
139
 
135
- `agent.tools(arr)` is the bulk-register companion to `agent.tool(t)`.
136
- Pair with `await client.tools()` to register everything an MCP server
137
- exposes in one builder call. Tool-name uniqueness is still validated
138
- at `.build()` across MCP servers + manual `.tool()` calls.
140
+ **The authority rule.** A tool result is written into the conversation once and then only
141
+ ages; the system prompt is rebuilt from nothing every iteration (`reactMode: 'dynamic'`,
142
+ the default, re-runs the InjectionEngine and all three slots). So standing instructions
143
+ belong in the recomposed surface. `reactMode: 'classic'` caches system-prompt and tools
144
+ after turn 1 — do **not** use it with skills.
139
145
 
140
- ### RAG — `defineRAG` (one factory, one helper)
146
+ ### RAG — retrieve, augment, generate
141
147
 
142
148
  ```typescript
143
- import {
144
- defineRAG, indexDocuments,
145
- InMemoryStore, mockEmbedder,
146
- } from 'agentfootprint';
147
-
148
- const embedder = mockEmbedder();
149
- const store = new InMemoryStore();
150
-
151
- // Seed the corpus once at startup
152
- await indexDocuments(store, embedder, [
153
- { id: 'doc1', content: 'Refunds are processed within 3 business days.' },
154
- { id: 'doc2', content: 'Pro plan costs $20/month.' },
155
- ]);
156
-
157
- // Define the retriever
158
- const docs = defineRAG({
159
- id: 'product-docs',
160
- store, embedder,
161
- topK: 3,
162
- threshold: 0.7, // STRICT — nothing is injected when nothing matches
163
- });
164
- // Retrieved chunks land in the SYSTEM-PROMPT slot, as one system message
165
- // of citable `<source id=… doc=… score=…>` blocks.
166
- // `asRole` was removed in 7.20.0 — it was never read, and passing it throws.
167
-
168
- // Wire to agent — `.rag()` is an alias for `.memory()`, same plumbing
169
- agent.rag(docs);
149
+ import { defineRAG } from 'agentfootprint'; // wiring lives on the main barrel
150
+ import { InMemoryStore, mockEmbedder } from 'agentfootprint/memory';
170
151
 
171
- // No identity argument anywhere: a corpus lives in its own namespace
172
- // (`corpus`, default `{ conversationId: '_global' }`) — the same one
173
- // indexDocuments writes to.
174
- await agent.run({ message: 'How long do refunds take?' });
152
+ Agent.create({ provider, model })
153
+ .rag(defineRAG({ id: 'docs', store: new InMemoryStore(), embedder: mockEmbedder(), topK: 5 }))
154
+ .build();
175
155
  ```
176
156
 
177
- **Building the index** (8.10.0) is `agentfootprint/rag`: `indexFolder('./docs', { to: store, embedder })`, or the `loadDocuments` → `splitDocuments` → `indexCorpus` pieces, or `npx agentfootprint-index ./docs --to ./corpus.db`. Loaders for text/Markdown/HTML (zero-dep) and PDF (lazy `unpdf`, per-page text so citations can name a page). Re-running embeds only what changed. `defineRAG` stays on the MAIN barrel — it is run-time wiring; that door is index time.
178
-
179
- `defineRAG` runs on `defineMemory({ type: SEMANTIC, strategy: TOP_K })`. Same machinery, three deliberate differences: a corpus is **read-only** (it never stores the conversation), it reads under its **own namespace** rather than the run's identity, and its chunks render as **citable `<source>` blocks**. For conversation memory alongside a corpus, register both — `.rag(defineRAG(...))` and `.memory(defineMemory(...))`, each with its own store.
180
-
181
- **Why did the agent read this passage?** `agentfootprint.memory.retrieved` carries every candidate with its score — including the ones that were rejected and why. `agentfootprint.memory.attached` fires per chunk that reached the prompt. `agentfootprint.context.injected` reports `source: 'rag'` with that chunk's `retrievalScore` / `rankPosition` / `threshold`. The whole record is on root state as `retrievalEvidence_<id>`, where a backward slice can reach it.
182
-
183
- ### Agent (ReAct primitive)
157
+ ### Control flow + patterns — compose runners
184
158
 
185
159
  ```typescript
186
- import { Agent, defineTool } from 'agentfootprint';
187
- import { anthropic } from 'agentfootprint/providers';
188
-
189
- const agent = Agent.create({
190
- provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }),
191
- model: 'claude-sonnet-4-5-20250929',
192
- maxIterations: 10,
193
- })
194
- .system('You are a helpful assistant.')
195
- .tool(weatherTool)
196
- .build();
160
+ import { Sequence, Parallel, Loop, Conditional, workflow, graph } from 'agentfootprint';
161
+ import { swarm, debate, reflection, selfConsistency, mapReduce, tot } from 'agentfootprint'; // patterns
197
162
 
198
- const result = await agent.run({ message: 'Weather in SF?' });
199
- ```
163
+ const pipeline = Sequence.create().step('research', researchAgent).step('write', writerAgent).build();
200
164
 
201
- Builder methods:
202
- - `.system(prompt)` — base system prompt
203
- - `.tool(definedTool)` — register a tool
204
- - `.steering(injection)` · `.instruction(injection)` · `.skill(injection)` · `.fact(injection)` — context-engineering injections
205
- - `.memory(definition)` — register a memory (returned by `defineMemory()`)
206
- - `.build()` → `Agent` — runner with `.run({ message, identity? })`
165
+ const desk = swarm({
166
+ agents: [{ id: 'research', runner: researchAgent }, { id: 'write', runner: writerAgent }],
167
+ route: ({ message }) => (/write/.test(message) ? 'write' : 'research'),
168
+ });
169
+ ```
207
170
 
208
- ### LLMCall (one-shot primitive)
171
+ ## Providers
209
172
 
210
173
  ```typescript
211
- import { LLMCall } from 'agentfootprint';
212
- import { anthropic } from 'agentfootprint/providers';
213
-
214
- const call = LLMCall.create({ provider: anthropic(...), model: 'claude-sonnet-4-5-20250929' })
215
- .system('You are a terse assistant.')
216
- .build();
174
+ import { mock, anthropic, openai, bedrock, ollama } from 'agentfootprint/providers';
217
175
 
218
- const answer = await call.run({ message: 'Summarize: ...' });
176
+ const provider = process.env.NODE_ENV === 'production'
177
+ ? anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! })
178
+ : ollama('llama3.2'); // free local model; or mock({...}) for determinism
219
179
  ```
220
180
 
221
- ### Tools
181
+ `mock` takes `{ reply }` (one fixed answer), `{ replies: [...] }` (consumed in order — exhaustion throws loud), or `{ respond: (req) => … }` (build the answer from the request, including `toolCalls`).
182
+
183
+ ## Tools
222
184
 
223
185
  ```typescript
224
186
  import { defineTool } from 'agentfootprint';
225
187
 
226
- const weather = defineTool({
227
- name: 'weather',
228
- description: 'Current weather for a city.',
229
- inputSchema: {
230
- type: 'object',
231
- properties: { city: { type: 'string' } },
232
- required: ['city'],
233
- },
234
- execute: async (args) => `${(args as { city: string }).city}: 72°F`,
188
+ const calculator = defineTool({
189
+ name: 'calculator', // `name`, not `id`
190
+ description: 'Perform arithmetic',
191
+ inputSchema: { type: 'object', properties: { expression: { type: 'string' } } },
192
+ execute: async ({ expression }: { expression: string }) => String(evaluate(expression)),
235
193
  });
236
194
  ```
237
195
 
238
- Tool-name uniqueness is validated at `agent.build()` time across `.tool()` registrations AND every Skill's `inject.tools[]`.
239
-
240
- A long-running tool can say where it is, mid-call, with `ctx.progress(payload)` —
241
- each call files one `agentfootprint.stream.tool_progress` event between that
242
- call's `tool_start` and `tool_end`, so a forty-second walk reads as "hop 3 of 12"
243
- instead of silence:
196
+ ### A long tool that says where it is
244
197
 
245
198
  ```typescript
246
199
  execute: async (args, ctx) => {
247
200
  for (const [i, hop] of hops.entries()) {
248
201
  await visit(hop);
249
- ctx.progress({ done: i + 1, total: hops.length }); // telemetry, never the model's to read
202
+ ctx.progress({ done: i + 1, total: hops.length }); // → stream.tool_progress
250
203
  }
251
204
  return summarize(hops);
252
205
  },
253
206
  ```
254
207
 
255
- The framework stamps `toolCallId` / `toolName` / `iteration`; `payload` is yours,
256
- forwarded verbatim (it must survive `structuredClone`). Always present, never
257
- throws, never blocks — with nothing listening it is a no-op, and a tool that
258
- never calls it emits exactly what it emitted before.
259
-
260
- ### Context Engineering — 4 typed factories
261
-
262
- All four return an `Injection` evaluated by the same engine; all emit the same `agentfootprint.context.injected` event with `source` discriminating the flavor.
263
-
264
- ```typescript
265
- import {
266
- defineSkill, defineSteering, defineInstruction, defineFact,
267
- } from 'agentfootprint';
268
-
269
- // Always-on rule (system-prompt)
270
- const tone = defineSteering({
271
- id: 'tone',
272
- prompt: 'Be friendly and concise.',
273
- });
274
-
275
- // Predicate-gated
276
- const urgent = defineInstruction({
277
- id: 'urgent',
278
- activeWhen: (ctx) => /urgent|asap/i.test(ctx.userMessage),
279
- prompt: 'Prioritize the fastest path to resolution.',
280
- });
281
-
282
- // Dynamic ReAct — fires AFTER a specific tool returned (system slot, that turn only)
283
- const afterRedact = defineInstruction({
284
- id: 'after-redact',
285
- activeWhen: (ctx) => ctx.lastToolResult?.toolName === 'redact_pii',
286
- prompt: 'Use the redacted text only. Do not paraphrase the original.',
287
- });
288
- // `slot: 'messages'` DELIVERS since 7.21.0 — it appends to `scope.history`, so the
289
- // window strategies, the trace and the wire all see it. It requires a `role` (no
290
- // default), and both wire rules can refuse: a role the provider does not carry
291
- // inside `messages` throws at RUN START naming the provider (Anthropic-family
292
- // drops 'system' there, OpenAI-family carries it), and a role that would repeat
293
- // the turn at the end of the window is DEFERRED to the next boundary with a
294
- // reason on `messagesDelivery.deferred`. Practical consequence: inside a
295
- // tool-using loop `role: 'user'` typically never delivers (the window ends on
296
- // the user's turn, or on tool results, which count as one) — use 'assistant', or
297
- // return the words from the tool itself.
298
-
299
- // LLM-activated body + tools (auto-attaches `read_skill` activation tool)
300
- const billing = defineSkill({
301
- id: 'billing',
302
- description: 'Use for refunds, subscriptions, invoices.',
303
- body: 'Confirm identity before processing refunds.',
304
- tools: [refundTool],
305
- });
306
- // A declarative `skillGraph()` is fully shipped (lib/injection-engine/skillGraph.ts):
307
- // fluent builder — .entry(skill,{when?}) .route(from,to,{when|onToolReturn}) .tree(decide(...))
308
- // .build() + toMermaid() — or the config form skillGraph({skills, start, steps} | {skills, tree}).
309
- // The v2 hardening ALL shipped versions ago: from-gating via currentSkillId, the scoped
310
- // read_skill gate (toolCalls.ts), per-matcher try/catch, entry scorers (entryBy/entryByRelevance/
311
- // entryByRead), governors + routeRecorder, and build-time validation (graph.checkup()).
312
- // Since 8.3.0 an accepted read_skill pick really activates (and can move the cursor);
313
- // since 8.4.0 invalid combinations refuse at build() with teaching messages, and skills the
314
- // graph doesn't route ("open skills": llm-activated + no incoming edge) activate via read_skill
315
- // without moving the cursor — so .selfExplain() works alongside a graph.
316
- // Spec: docs/design/skill-graph-spec.md; guide: docs/skill-graph-guide.md.
317
-
318
- // Developer-supplied data (not behavior)
319
- const userProfile = defineFact({
320
- id: 'user',
321
- data: 'User: Alice (alice@example.com), Plan: Pro.',
322
- });
323
-
324
- agent
325
- .steering(tone)
326
- .instruction(urgent)
327
- .instruction(afterRedact)
328
- .skill(billing)
329
- .fact(userProfile);
330
- ```
331
-
332
- ### Memory — `defineMemory({ type, strategy, store })`
208
+ `ctx.progress` is always present, never throws, never blocks, and never reaches
209
+ the model — the framework stamps `toolCallId` / `toolName` / `iteration`, you own
210
+ `payload`. `agent.on('agentfootprint.stream.*')` and `toSSE(agent)` carry it with
211
+ no extra wiring.
333
212
 
334
- ONE factory dispatches `type × strategy.kind` onto the right pipeline. Multiple memories layer cleanly via per-id scope keys (`memoryInjection_${id}`).
213
+ ### When a tool finds nothing, and what a clean result does not cover
335
214
 
336
215
  ```typescript
337
- import {
338
- defineMemory,
339
- MEMORY_TYPES, MEMORY_STRATEGIES, SNAPSHOT_PROJECTIONS,
340
- InMemoryStore, mockEmbedder,
341
- } from 'agentfootprint';
342
-
343
- // Short-term sliding window — the 90% case
344
- const shortTerm = defineMemory({
345
- id: 'short-term',
346
- type: MEMORY_TYPES.EPISODIC,
347
- strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
348
- store: new InMemoryStore(),
349
- });
350
-
351
- // Semantic recall — vector retrieval with strict threshold
352
- const facts = defineMemory({
353
- id: 'facts',
354
- type: MEMORY_TYPES.SEMANTIC,
355
- strategy: {
356
- kind: MEMORY_STRATEGIES.TOP_K,
357
- topK: 3,
358
- threshold: 0.7, // STRICT — empty when no match
359
- embedder: mockEmbedder(), // swap for openaiEmbedder() in prod
360
- },
361
- store: new InMemoryStore(),
362
- });
363
-
364
- // Causal — UNIQUE TO AGENTFOOTPRINT. Persists run snapshots so cross-run
365
- // "why was X rejected?" follow-ups answer from the STORED run: decisions
366
- // (footprintjs decide()/select() evidence + route/skill-graph provenance),
367
- // tool calls (args + result previews), iterations, duration, token usage —
368
- // harvested automatically by causalEvidenceRecorder when a CAUSAL memory is
369
- // mounted. (commitLog/narrative capture: not yet — see SnapshotEntry.)
370
- const causal = defineMemory({
371
- id: 'causal',
372
- type: MEMORY_TYPES.CAUSAL,
373
- strategy: {
374
- kind: MEMORY_STRATEGIES.TOP_K,
375
- topK: 1,
376
- threshold: 0.7,
377
- embedder: mockEmbedder(),
378
- },
379
- store: new InMemoryStore(),
380
- projection: SNAPSHOT_PROJECTIONS.DECISIONS,
381
- });
216
+ import { absent, coverage, defineTool } from 'agentfootprint';
382
217
 
383
- agent.memory(shortTerm).memory(facts).memory(causal);
218
+ // "I looked and there is nothing" — never readable as "I could not look".
219
+ execute: ({ port }) => rows.length ? rows : absent({
220
+ what: `FLOGI entries on ${port}`,
221
+ checked: ['shq-fab-a: the live fcns database', 'window: the last 24h'],
222
+ notChecked: [{ what: 'the archived history', why: 'older than the 24h window' }],
223
+ cannotCover: [{ what: 'the peer fabric', why: 'this collector is scoped to one fabric' }],
224
+ tryInstead: 'Ask for a different interface, or query the peer fabric by name.',
225
+ }),
384
226
 
385
- // Multi-tenant identity is plumbed through agent.run:
386
- await agent.run({
387
- message: '...',
388
- identity: { tenant: 'acme', principal: 'alice', conversationId: 'thread-42' },
389
- });
227
+ // A verdict WITH its boundary — what "fine" does and does not rule out.
228
+ execute: async () => coverage(await checkReplication(), {
229
+ checked: ['SRDF pair state on all 4 arrays'],
230
+ cannotCover: [{ what: 'host-side multipathing', why: 'no collector on the ESX hosts' }],
231
+ }),
390
232
  ```
391
233
 
392
- The 4 memory **types**:
393
- - `EPISODIC` — raw conversation messages
394
- - `SEMANTIC` — extracted structured facts
395
- - `NARRATIVE` — beats / summaries of prior runs
396
- - `CAUSAL` — footprintjs decision-evidence snapshots ⭐
397
-
398
- The 7 **strategies**:
399
- - `WINDOW` (rule, last N) · `BUDGET` (decider, fit-to-tokens) · `SUMMARIZE` (one LLM call folds the older entries; the summary is stored, so a span is paid for once — needs `llm` + `model`)
400
- - `TOP_K` (score-threshold) · `EXTRACT` (LLM distills on write)
401
- - `DECAY` (recency-weighted, planned) · `HYBRID` (compose multiple)
234
+ An absence gets the delivered status `'absent'` (route it with
235
+ `onToolStatus: 'absent'`), files `agentfootprint.tools.absent`, and grounds only
236
+ its COVERAGE in the evidence gate — so an id the model invented does not become
237
+ grounded by one lookup that found nothing. It is never an error: nothing retries
238
+ it, nothing refuses it, no `error: true`. A ledger files
239
+ `agentfootprint.tools.coverage_declared`; add `.limitsTravelWithTheAnswer()` on
240
+ the agent and the framework APPENDS the run's limits to the final answer, so the
241
+ model cannot drop what it never wrote.
402
242
 
403
- ### Compositions — Multi-Agent via Control Flow
404
-
405
- There is **no** `MultiAgentSystem` class. Multi-agent = compositions of single Agents through the same control flow that connects any flowchart stages:
243
+ ## Observing a run
406
244
 
407
245
  ```typescript
408
- import { Sequence, Parallel, Conditional, Loop } from 'agentfootprint';
409
-
410
- // Output flows downstream — every step needs an id + a runner
411
- const pipeline = Sequence.create()
412
- .step('research', researcher) // each runner is an Agent / LLMCall / composition
413
- .step('write', writer)
414
- .step('edit', editor)
415
- .build();
416
-
417
- // Multi-perspective with LLM merge — branches need ids; rank via mergeWithLLM
418
- const tot = Parallel.create()
419
- .branch('a', thoughtAgent)
420
- .branch('b', thoughtAgent)
421
- .branch('c', thoughtAgent)
422
- .mergeWithLLM({ provider, model: 'mock', prompt: 'Pick the best answer.' })
423
- .build();
424
- // (or .mergeWithFn((results) => ...) to merge in code)
246
+ import { costRecorder, routeRecorder } from 'agentfootprint/observe';
425
247
 
426
- // Predicate-based routing — .when(id, predicate, runner); .otherwise is mandatory
427
- const triage = Conditional.create()
428
- .when('billing', (input) => /refund|invoice/i.test(input.message), billingAgent)
429
- .when('tech', (input) => /error|bug/i.test(input.message), techAgent)
430
- .otherwise('general', generalAgent)
248
+ const agent = Agent.create({ provider, model })
249
+ .watch(routeRecorder({ id: 'routes' })) // recorders are factories, not classes
431
250
  .build();
432
251
 
433
- // Iterate with a REQUIRED budget — .repeat(body) + .times(n) / .forAtMost(ms) / .until(guard)
434
- const refine = Loop.create()
435
- .repeat(critiqueAgent)
436
- .until(({ iteration, latestOutput }) => latestOutput.includes('DONE'))
437
- .times(5)
438
- .build();
252
+ agent.on('agentfootprint.context.evaluated', (e) => console.log(e.payload.activeIds));
439
253
  ```
440
254
 
441
- ### Named patterns — recipes ship as runnable examples
442
-
443
- ```
444
- ReAct = Agent (default loop)
445
- Reflexion = Sequence(Agent, critique-LLM, Agent)
446
- Tree-of-Thoughts = Parallel(Agent × N) + rank
447
- Self-Consistency = Parallel(Agent × N) + majority-vote
448
- Debate = Loop(Agent × 2 + judge)
449
- Map-Reduce = Parallel(Agent × N) + merge
450
- Swarm = Agent whose tools are other Agents
451
- ```
452
-
453
- Browse [`examples/patterns/`](examples/patterns/) — every pattern is a runnable end-to-end test.
454
-
455
- ### Providers
255
+ **121 typed events across 27 domains.** Two subscription shapes and no third:
256
+ `'*'` (every event) and `'agentfootprint.<domain>.*'` (one domain). **`'agentfootprint.*'`
257
+ is not a pattern** — TypeScript rejects it, and at runtime it would match nothing.
456
258
 
457
259
  ```typescript
458
- import { mock, anthropic, openai, bedrock, ollama } from 'agentfootprint/providers';
459
-
460
- // The ladder: shape the logic on the mock → check it against a REAL model for
461
- // free → pay only in production. One argument changes; the agent does not.
462
- const provider = process.env.NODE_ENV === 'production'
463
- ? anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! })
464
- : ollama('llama3.2'); // or mock({ reply: 'test response' }) for determinism
260
+ agent.on('*', (e) => log(e));
261
+ agent.on('agentfootprint.stream.*', (e) => log(e)); // tool_start, tool_end, deltas
262
+ agent.on('agentfootprint.agent.turn_end', (e) =>
263
+ console.log(`${e.payload.iterationCount} iterations`));
465
264
  ```
466
265
 
467
- Every provider implements the same `LLMProvider` interface. `mock`,
468
- `browserAnthropic`, `browserOpenai`, and `createProvider` ship on the main
469
- barrel; `anthropic` · `openai` · `bedrock` · `ollama` live ONLY at
470
- `agentfootprint/providers` so bundlers never walk the vendor-SDK requires.
471
- Browser variants exist for client-side use.
472
-
473
- `ollama('<model>')` is the middle rung and needs NO vendor SDK — it talks
474
- Ollama's native API over `fetch`. Install Ollama, `ollama pull llama3.2`, done.
475
- When it can't work it says so in words that contain the fix (`ollama serve`,
476
- `ollama pull <model>`), never a raw connection error. `OLLAMA_MODEL=<model>`
477
- makes `providerFromEnv()` pick it, ahead of the cloud credentials.
478
-
479
- ### Pause / Resume (Human-in-the-Loop)
266
+ ## Human in the loop
480
267
 
481
268
  ```typescript
482
- import { askHuman, pauseHere, isPaused } from 'agentfootprint';
483
-
484
- const approveTool = defineTool({
485
- name: 'approve',
486
- description: 'Ask a human.',
487
- inputSchema: { type: 'object', properties: { amount: { type: 'number' } } },
488
- // askHuman() / pauseHere() throw a PauseRequest — CALL them inside
489
- // execute, don't pass them as the execute function.
490
- execute: async (args) =>
491
- askHuman({ question: `Approve $${(args as { amount: number }).amount}?` }),
492
- });
269
+ import { askHuman, isPaused, checkInApproved } from 'agentfootprint';
493
270
 
494
271
  const result = await agent.run({ message: 'Refund $500?' });
495
272
  if (isPaused(result)) {
496
- const checkpoint = result.checkpoint; // JSON-serializable
497
- // Persist to Redis/DB; later, on possibly different server:
498
- const final = await agent.resume(checkpoint, { approved: true });
273
+ const final = await agent.resume(result.checkpoint, checkInApproved({ by: 'maya' }));
499
274
  }
500
275
  ```
501
276
 
502
- ### Resilience
277
+ ## Resilience
503
278
 
504
279
  ```typescript
505
- import { withRetry, withFallback, fallbackProvider, withCircuitBreaker } from 'agentfootprint/resilience';
506
- import { anthropic, openai, ollama } from 'agentfootprint/providers';
280
+ import { withRetry, withFallback, withCircuitBreaker } from 'agentfootprint/resilience';
507
281
 
508
282
  const reliable = withRetry(provider, { maxAttempts: 3 });
509
- const resilient = withFallback(primary, fallback);
510
- const chain = fallbackProvider(anthropic({...}), openai({...}), ollama('llama3.2'));
511
- const guarded = withCircuitBreaker(provider);
283
+ const resilient = withFallback(primary, backup);
512
284
  ```
513
285
 
514
- Resilience decorators live on the `agentfootprint/resilience` subpath
515
- (not the main barrel). Each preserves the `LLMProvider` interface and
516
- stacks freely.
517
-
518
- ### Result primitives — an absence that names itself, a limit that travels
519
-
520
- ```typescript
521
- import { absent, coverage } from 'agentfootprint';
522
-
523
- // A tool that found nothing, in a shape no model can read as "I could not look".
524
- return absent({
525
- what: `FLOGI entries on ${port}`,
526
- checked: [`${sw}: the live fcns database`, 'window: the last 24h'],
527
- cannotCover: [{ what: 'the peer fabric', why: 'this collector is scoped to one fabric' }],
528
- tryInstead: 'Ask for a different interface, or query the peer fabric by name.',
529
- });
530
-
531
- // A verdict with its own boundary attached.
532
- return coverage(verdict, {
533
- checked: ['SRDF pair state on all 4 arrays'],
534
- notChecked: [{ what: 'NDM migration sessions', why: 'the API timed out' }],
535
- cannotCover: [{ what: 'host-side multipathing', why: 'no collector on the ESX hosts' }],
536
- });
537
- ```
538
-
539
- An absence carries the delivered status `'absent'` (routable by `onToolStatus`),
540
- files `agentfootprint.tools.absent`, and grounds only its coverage in the
541
- evidence gate. It is never an error — nothing retries or refuses it. A ledger
542
- files `agentfootprint.tools.coverage_declared`; with
543
- `.limitsTravelWithTheAnswer()` the framework appends the run's declared limits
544
- to the final answer, which is how a limit survives a model that would rather not
545
- mention it.
546
-
547
- ### Observability — 120 typed events across 27 domains
548
-
549
- ```typescript
550
- agent.on('agentfootprint.context.injected', (e) =>
551
- console.log(`[${e.payload.source}] landed in ${e.payload.slot}`));
552
- agent.on('agentfootprint.stream.tool_start', (e) =>
553
- console.log(`→ ${e.payload.toolName}(${JSON.stringify(e.payload.args)})`));
554
- agent.on('agentfootprint.agent.turn_end', (e) =>
555
- console.log(`[${e.payload.iterationCount} iter, ${e.payload.totalInputTokens}+${e.payload.totalOutputTokens} tokens]`));
556
- ```
557
-
558
- Wildcards: `.on('*', ...)` for every event, or `.on('agentfootprint.<domain>.*', ...)` per-domain (`agent`, `stream`, `context`, `tools`, `memory`, `cost`, `error`, …). `'agentfootprint.*'` is NOT a valid pattern — the dispatcher accepts `'*'` or `'agentfootprint.<DOMAIN>.*'` only. All events typed via `AgentfootprintEventMap`.
559
-
560
- Recorders (auto-attached when relevant builder method is called):
561
- - `ContextRecorder` — `context.evaluated` / `context.injected` / `context.slot_composed`
562
- - `streamRecorder` — `stream.llm_start` / `stream.llm_end` / `stream.token` / `stream.tool_start` / `stream.tool_end`
563
- - `agentRecorder` — `agent.turn_start` / `agent.turn_end` / `agent.iteration_start` / `agent.iteration_end` / `agent.route_decided`
564
- - `costRecorder` — `cost.tick` / `cost.limit_hit` (when `pricingTable` supplied)
565
- - `permissionRecorder` — `permission.check` (when `permissionChecker` supplied)
566
- - `evalRecorder` · `memoryRecorder` · `skillRecorder`
567
-
568
- **Observer delivery tier (RFC-001 Block 10):** `Agent.create({ observerDelivery:
569
- 'deferred' })` routes the bridge recorders above + consumer `.watch()` /
570
- `agent.attach()` recorders through footprintjs's bounded capture queue —
571
- capture inline (≈ µs), deliver one beat behind, drain synchronously at run
572
- resolve / reject / pause. Default `'inline'` = byte-identical attach path, no
573
- queue allocated. `agent.on()` listeners receive deep-equal typed events either
574
- way (parity-tested). EXCEPTION kept inline: the causal-evidence recorder — the
575
- memory write stage reads `collect()` MID-run. A recorder's own `delivery`
576
- field beats the agent default (per-recorder override). Dials via
577
- `observerDeliveryOptions` (throws without `'deferred'`); shutdown via
578
- `agent.drainObservers({ timeoutMs })`; stats on
579
- `getLastSnapshot()?.observerStats`. CONTRACT: typed event payloads must be
580
- detached plain data — never pass a TypedScope read (e.g. `scope.history`, a
581
- live deep-Proxy) into `typedEmit`; use the plain local value (`typedEmit`
582
- dev-warns on unclonable payloads). Bench:
583
- `examples/features/21-deferred-observers.ts`.
584
-
585
- ### Self-explain — `.selfExplain()` (the agent answers "why?" about its own last run)
586
-
587
- One builder call mounts ONE skill that, when the user asks a why-question, unlocks the 5 trace tools (`run_overview` / `trace_node` / `who_wrote` / `get_value` / `trace_slice`) for that iteration ONLY — bound LATE to the agent's PREVIOUS COMPLETED run (captured at `onRunEnd`/`onRunFailed`, so it can never serve an in-flight run; a failed run still explains "why did you fail?"). `delegate: { provider, model }` runs the trace-walk on a cheaper model via one `explain_run` tool. Requires `reactMode: 'dynamic'`/`'dynamic-grouped'` (classic caches the tools slot → throws at build); reserved tool-name clash also throws. Differs from **Causal memory**: `.selfExplain()` = THIS conversation's last run, in-memory, drill-by-id; Causal = any past run, persisted, recalled by similarity — reach for selfExplain on same-conversation follow-ups, Causal for cross-session. Note: Causal *similarity* recall needs a store with `search()` (only `InMemoryStore` today; Redis/AgentCore lack it). Full guide: docs `debug/self-explain`.
588
-
589
- ## Anti-Patterns — Don't
590
-
591
- - ❌ **Don't ship a `ReflexionAgent` class.** Compose `Sequence(Agent, critique-LLM, Agent)`.
592
- - ❌ **Don't use `agent.run('string')`** — use `agent.run({ message: '...', identity? })`.
593
- - ❌ **Don't import from non-existent subpaths** like `'agentfootprint/instructions'` — the injection factories live on the main barrel (or `'agentfootprint/context'`). NOTE: `'agentfootprint/observe'`, `'agentfootprint/security'`, `'agentfootprint/resilience'`, `'agentfootprint/providers'`, `'agentfootprint/memory'`, `'agentfootprint/providers'`, `'agentfootprint/observe'` ARE real subpaths — some surfaces (vendor providers, resilience decorators) live ONLY there, not on the main barrel.
594
- - ❌ **Don't use `.memoryPipeline(pipeline)`** — that's the v1 API. Use `.memory(defineMemory({...}))`.
595
- - ❌ **Don't fall back when TopK threshold returns nothing.** Strict semantics: garbage past context > none is wrong.
596
- - ❌ **Don't store closures or class instances in scope** — TransactionBuffer can't clone functions. Memory-store entries serialize to JSON.
597
- - ❌ **Don't add new event types per feature.** Route through `agentfootprint.context.injected` with a new `source` value.
598
- - ❌ **Don't reach into `getArgs()` / `getEnv()` from injection content.** Predicates run with the engine's `InjectionContext` only.
599
-
600
286
  ## Decision Tree — Pick the Right Tool
601
287
 
602
288
  | Goal | Use |
@@ -620,43 +306,34 @@ One builder call mounts ONE skill that, when the user asks a why-question, unloc
620
306
  | Retrieve from a document corpus | `defineRAG({ store, embedder, topK, threshold })` |
621
307
  | Use tools from an external MCP server | `mcpClient({ transport, ... })` + `agent.tools(await c.tools())` |
622
308
 
623
- ## Build & Test
309
+ ## Anti-Patterns
624
310
 
625
- ```bash
626
- npm install agentfootprint footprintjs # footprintjs ^6 is a peer dependency
627
- npm test # vitest run
628
- npm run example examples/... # run a single example end-to-end
629
- npm run test:examples # typecheck + run every example
630
- ```
311
+ - Don't use `id`/`handler` on `defineTool` — it's `name`/`execute`
312
+ - Don't call `mock([...])` with an array — it's `mock({ replies: [...] })`
313
+ - Don't import any provider (`mock` included) from the main barrel — they live on `agentfootprint/providers`
314
+ - Don't `new` a recorder — they're lowercase factories attached via `.watch()`
315
+ - Don't write `.entry(x, { when: () => true })` for an always-on skill — omit `when`, or use `.steering()`
316
+ - Don't post-process execution — use recorders
631
317
 
632
- ## Package layout
318
+ ## Checking your own wiring
633
319
 
634
- ```
635
- src/
636
- ├── core/ — Agent, LLMCall, builder methods, pause/resume
637
- ├── core-flow/ — Sequence, Parallel, Conditional, Loop
638
- ├── patterns/ — Reflexion, SelfConsistency, ToT, Debate, MapReduce, Swarm
639
- ├── lib/
640
- │ └── injection-engine/ — Injection primitive + 4 factories + engine subflow
641
- ├── memory/ — defineMemory + 4 types × 7 strategies + InMemoryStore + Causal
642
- ├── adapters/llm/ — Anthropic, OpenAI, Bedrock, Ollama, Browser variants, Mock
643
- ├── recorders/ — context, stream, agent, cost, skill, permission, eval, memory
644
- ├── resilience/ — withRetry, withFallback, fallbackProvider, withCircuitBreaker
645
- └── stream.ts — SSE formatter
646
-
647
- examples/ — runnable end-to-end tests organized by DNA layer
648
- ├── core/ — primitives
649
- ├── core-flow/ — compositions
650
- ├── patterns/ — canonical recipes
651
- ├── context-engineering/ — InjectionEngine flavors
652
- ├── memory/ — 7 strategies
653
- └── features/ — pause/cost/permissions/observability/events
320
+ ```bash
321
+ npx agentfootprint-lint-tools tools.json # confusable tool catalog — the CI gate
322
+ npx agentfootprint-index ./docs --to ./corpus.db # build a RAG corpus at boot time
654
323
  ```
655
324
 
656
- ## Roadmap (informs what to defer)
325
+ ```typescript
326
+ const report = graph.checkup({ knownTools: ['lookup_order'] }); // unreachable skills, unknown edges, dead entries
327
+ if (!report.ok) throw new Error(formatCheckup(report));
328
+ ```
657
329
 
658
- - **v3.1 (current)** — primitives + compositions + InjectionEngine + Memory (incl. Causal) + providers + RAG (`defineRAG`) + MCP (`mcpClient`) + Redis/AgentCore memory adapters + resilience (retry / fallback / circuit breaker) + Permission policy + observability subsystem
659
- - **Shipped since v2.0** — RAG flavor · Redis memory adapter · MCP integration · CircuitBreaker · governance (PermissionPolicy)
660
- - **Planned** — Causal training-data exports (SFT / DPO / process-RL) · DynamoDB / Postgres / Pinecone adapters · Deep Agents · A2A protocol · Lens UI integration
330
+ ## Going deeper
661
331
 
662
- When in doubt — read [`examples/`](examples/), every file is a runnable spec.
332
+ The full architecture of the skill graph — the three surfaces, the authority rule, the
333
+ nine cursor causes, the three-way `read_skill`, and a worked refusal taken from a real
334
+ run — is published as **Skill graph architecture**:
335
+ <https://agentfootprint.dev/docs/build/skill-graph-architecture/>.
336
+ Every capability claim there carries a status — `shipped` / `opt-in` /
337
+ `application-provided` / `planned` — and every code block is type-checked against the
338
+ shipped types at build. Read it rather than this file when the question is "how does
339
+ routing actually work"; read this file for what to reach for and what does not exist.