agentfootprint 9.115.0 → 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.
package/README.md CHANGED
@@ -37,20 +37,6 @@
37
37
 
38
38
  ---
39
39
 
40
- ## Map, Walker, Trace, Fold, Lens
41
-
42
- Five words for the five jobs every file in this library does.
43
-
44
- - **Map** — what could happen: the chart an `Agent` or `LLMCall` declares, the skills and the edges between them. Still; a run never changes it.
45
- - **Walker** — what is happening: the ReAct loop, one stage at a time, with its cursor and its guards.
46
- - **Trace** — what happened and why: the typed event stream and the commit log, written as the run happens, never reconstructed after.
47
- - **Fold** — what may be claimed now: everything derived from that record — the evidence corpus, the reachable set, the influence ranking, the context ledger — each computed by one owner and handed out detached.
48
- - **Lens** — what this reader is served: for an agent that means **the wire** — the system prompt, the messages and the tool list the model actually gets, plus every sentence composed into them. A Lens may leave something out; it may never say the run holds nothing when it does.
49
-
50
- Every folder under `src/` opens its README with its role. The map of the whole tree, with the laws and worked examples, is [docs/design/map-walker-trace-fold-lens.md](docs/design/map-walker-trace-fold-lens.md).
51
-
52
- ---
53
-
54
40
  ## The new error class
55
41
 
56
42
  For decades, software had two kinds of errors — and developers never needed deep
@@ -117,152 +103,9 @@ const result = await agent.run({ message: 'Weather in Paris?' });
117
103
  console.log(result); // → "I checked: it is 72°F and sunny."
118
104
  ```
119
105
 
120
- For production, import a real provider from `agentfootprint/providers` and swap it in — `anthropic(...)` / `openai(...)` / `bedrock(...)` / `gemini(...)` / `ollama(...)`. Only the import line changes; the agent code stays the same. (The vendor-SDK providers live on the `agentfootprint/providers` subpath so the main `agentfootprint` barrel stays free of optional peer-dep requires; `mock`, `browserAnthropic`, and `browserOpenai` are on the main barrel.)
121
-
122
- ### Run against a local model — the free rung between the mock and the bill
123
-
124
- No cloud account, no API key, no vendor SDK, $0 per token:
125
-
126
- ```typescript
127
- import { Agent } from 'agentfootprint';
128
- import { ollama } from 'agentfootprint/providers';
129
-
130
- const agent = Agent.create({ provider: ollama('llama3.2'), model: 'llama3.2' }).build();
131
- // → talks to http://localhost:11434 (run `ollama pull llama3.2` first)
132
- ```
133
-
134
- This is the step that makes "the test run and the production run are the same code path" more than a slogan. A mock proves your control flow; it can't tell you whether a real model calls your tool, or what it makes of a tool description you wrote in a hurry. A local model can — and because it's free, you'll actually check before you pay.
135
-
136
- `ollama()` talks Ollama's native API directly, so there's nothing to install on this side, streamed calls report real token counts (so `.compaction()` and cost budgets work), and when it can't work it says why in words that contain the fix — `ollama serve` when nothing is listening, `ollama pull <model>` when the model isn't there, never a raw connection error and never a hang.
137
-
138
- For llama.cpp's `llama-server`, vLLM, Together or Groq, use `openai({ baseURL: 'http://localhost:8080/v1', apiKey: 'not-needed', defaultModel: '…' })` — any server speaking the OpenAI Chat Completions API, same `Agent` code either way. Full recipes: [Ollama guide](https://agentfootprint.dev/docs/build/ollama/) · [OpenAI-compatible endpoints](https://agentfootprint.dev/docs/build/openai/#openai-compatible-endpoints-ollama-llamacpp-vllm-together-groq-lm-studio).
139
-
140
- ### Then add context
141
-
142
- A real agent carries more than one prompt and one tool: facts about the user, always-on rules, skills that unlock on demand. Declare each piece — the framework decides **when** it fires and **which slot** it lands in, and every piece is born tracked:
143
-
144
- ```typescript
145
- import { defineFact, defineSteering, defineSkill } from 'agentfootprint/context';
146
-
147
- const agent = Agent.create({ provider, model })
148
- .system('You are a support agent.')
149
- .fact(defineFact({ // data the model should know — always on
150
- id: 'user-profile',
151
- data: 'Name: Maya · Plan: Pro · Customer since 2022',
152
- }))
153
- .steering(defineSteering({ // rules the model must follow — always on
154
- id: 'refund-policy',
155
- prompt: 'Never promise a refund before checking the policy tool.',
156
- }))
157
- .skill(defineSkill({ // guidance the LLM loads when it asks
158
- id: 'billing',
159
- description: 'Use for refunds, charges, billing questions.',
160
- body: 'When handling billing: confirm identity first, then…',
161
- tools: [refundTool],
162
- autoActivate: 'currentSkill', // ...and scope its tools to that window too
163
- }))
164
- .build();
165
- ```
166
-
167
- Same shape for `.instruction()` / `.memory()` / `.rag()` / raw `.injection()` — they're all the one primitive, `Injection = slot × trigger × cache`. [The full model ↓](#the-model--what-we-abstract)
168
-
169
- ### The SkillMap — declare which skills connect
170
-
171
- When an agent has several skills, the routing between them is a thing you declare, not prose you hope the model follows. The official names, in one sentence: **you declare the SkillMap; the agent is the SkillWalker; the recording carries both.**
172
-
173
- 1. **DECLARE** — `defineSkillMap` (a permanent alias of `skillGraph` — same function, both names forever): the skills, the edges between them, entry matchers as data (`match:`), and — since 9.51.0 — **guards as data** on route edges (`guard:`).
174
- 2. **ATTACH** — `.skillGraph(map)`. There is no walker class to construct: the agent IS the SkillWalker, moving its cursor over your map.
175
- 3. **WATCH** — every recording carries the map (`skill.graph_declared`, guard conditions included) and the walk (`cursorMove` on every iteration, with the evidence that opened or closed each guarded hop). The SkillGraph debugger renders both.
176
-
177
- The walker moves by exactly three movers:
178
-
179
- | mover | who decides | on the record |
180
- |---|---|---|
181
- | **llm** | the model picks via `read_skill`, bounded by the gate | `by: 'model-pick'`; refusals as `skill.rejected` |
182
- | **guard** | **your data decides** — a `when` / `onToolReturn` / `onToolStatus` / `guard:` edge fires | `by: 'route'`, with `cursorMove.guard` evidence when a data guard judged it |
183
- | **linear** | no choice — a hand-off that fires every time its source finishes | `by: 'route'` |
184
-
185
- A `guard:` is the data twin of the `when` predicate (at most one of the two per edge): conditions over the hop (`toolName`, `status`, `iteration`, …) and over the tool result's own JSON fields, in footprintjs's filter grammar (`eq/ne/gt/gte/lt/lte/in/notIn`, all ANDed):
186
-
187
- ```typescript
188
- const map = defineSkillMap()
189
- .entry(triage, { match: { keywords: ['check', 'account'] } })
190
- .route(triage, escalation, {
191
- onToolReturn: 'assess_risk',
192
- guard: { riskLevel: { in: ['high', 'critical'] }, score: { gte: 0.7 } },
193
- })
194
- .route(escalation, wrapup, { onToolReturn: 'close_case' })
195
- .build();
196
- ```
197
-
198
- Because the guard is data: the build **proves contradictions** (`guard-unsatisfiable` — a guard that can never pass refuses to build, naming the conflict), `map.toMermaid()` captions the edge (`on assess_risk when riskLevel in [high, critical] AND score ≥ 0.7`), and every evaluation that decides a hop — taken or refused — puts its per-condition evidence on the record (`cursorMove.guard` / `cursorMove.guardsClosed`), so "why didn't my edge fire?" is a lookup, not a debugging session. [Runnable end to end](examples/features/64-skill-map-guards.ts) · [The five-minute walkthrough →](docs-next/content/docs/build/skill-graph-quickstart.mdx)
199
-
200
- When a skill's playbook is really a **sequence**, declare it as data and the framework walks it — offering one step's tool at a time (banner-led: `[Step 2 of 6 — confirm the duplicate charge]`), with `skip_step` to put a decline on the record and one teaching nudge if the model stops early. Your other tools stay available throughout — a declared order, not a cage:
201
-
202
- ```typescript
203
- defineSkill({
204
- id: 'refund',
205
- description: 'Handles refunds end to end, by declared procedure.',
206
- body: 'Follow the refund procedure. Every step says why it exists.',
207
- tools: [findOrder, checkHistory, issueRefund, fileReceipt],
208
- steps: [ // the procedure, as data (9.18.0)
209
- { tool: 'find_order', note: 'find the order before touching money' },
210
- { tool: 'check_history', note: 'confirm the duplicate charge' },
211
- { tool: 'issue_refund', note: 'refund the duplicate charge only' },
212
- { tool: 'file_receipt', note: 'file the receipt for audit' },
213
- ],
214
- });
215
- ```
216
-
217
- Every move lands on the typed stream (`skill.step_advanced` / `step_skipped` / `steps_unfinished`), and a step whose tool asks a human pauses the run and advances on resume — [runnable end to end](examples/context-engineering/16-skill-steps.ts).
106
+ For production, import a real provider from `agentfootprint/providers` and swap it in — `anthropic(...)` / `openai(...)` / `bedrock(...)` / `gemini(...)` / `ollama(...)`. Only the import line changes; the agent code stays the same. (Every provider — `mock` included — lives on the `agentfootprint/providers` subpath, so the main `agentfootprint` barrel stays free of optional peer-dep requires.)
218
107
 
219
- **The cursor picks the brain, too (9.19.0).** The graph already decides *where* the run is; `defineSkill({ provider, model })` — or `skillGraph(g, { providers })` — lets that position decide *who answers*: triage on the small model, the refund skill on the strong one, `llm_start.brain` recording which rung won. Add `escalation: { provider, model, afterRefusals: N }` and N recorded routing refusals in one turn flip the rest of it onto the bigger brain (`skill.escalated` — evidence, never vibes), and `decider: { provider, model }` resolves an ambiguous turn-start menu out of band (`turn_routed { by: 'decider' }` — the sanctioned resolver for `rails`). And tools steer with **data, not string conventions**: return `{ content, status, effects }` and a `'denied'` refund routes by a declared `onToolStatus` edge — meaning, not prose — while a `propose-transition` / `require-instruction` effect is validated against the graph's own law, every acceptance and teaching refusal a typed `tools.effect` event. [Runnable end to end](examples/context-engineering/17-brains-and-tool-effects.ts)
220
-
221
- ### Then keep the conversation
222
-
223
- `run()` is **one turn**. It seeds the conversation from the message you pass and nothing else, so calling it twice gives you two conversations — right for one-shot work, and not what a chat wants. Continuing is something you name:
224
-
225
- ```typescript
226
- await agent.run({ message: 'Book me a table for two on Friday.' });
227
- await agent.followUp('Make it three.'); // same conversation
228
-
229
- // …or hand the conversation around: plain JSON, any store, any machine.
230
- const conversation = agent.checkpoint();
231
- await agent.run({ message: 'Make it three.', continueFrom: conversation });
232
- ```
233
-
234
- The conversation carries its own `identity`, so a continued turn writes its memory where the earlier turns can read it. Two things that *look* like this and are not: `identity.conversationId` is a namespace key (it scopes memory, RAG and permissions — it does not join two runs), and `.memory()` gives you **recall** in the system prompt rather than the verbatim window. `standingAgent({ agent, sessions, host })` does the whole store-and-continue dance per session for you. [Worked example, printing the wire each way →](examples/features/51-conversations.ts)
235
-
236
- **The conversation keeps its place, too (9.17.0).** On a skill graph mounted with `.skillGraph(graph, { continuity: 'conversation' })`, the skill the last turn ended on rides the same checkpoint — `followUp('one more thing')` starts *there* instead of re-routing from scratch. A sticky default, never a lock: each new message is still judged by the graph's declared rules and intents (`match: { intent, examples }` + `classify:` a scorer), a decisively different topic moves, a genuinely ambiguous one offers the model a menu, and every verdict — winners, losers, the thresholds that judged them — is recorded as `agentfootprint.skill.turn_routed`. [The routing cascade, in five minutes →](docs-next/content/docs/build/skill-graph-quickstart.mdx) · [Runnable example →](examples/context-engineering/15-skill-graph-intents.ts)
237
-
238
- ### Then compose control flow
239
-
240
- One agent is a `Runner`. So is every composition of agents — four control-flow primitives, and anything that runs composes into anything else:
241
-
242
- ```typescript
243
- import { Sequence, Parallel, Conditional } from 'agentfootprint';
244
-
245
- const pipeline = Sequence.create()
246
- .step('classify', classifyAgent) // sequence: step → step
247
- .step('review',
248
- Parallel.create() // parallel: fan out, then merge
249
- .branch('legal', legalAgent)
250
- .branch('ethics', ethicsAgent)
251
- .mergeWithLLM({ provider, model, prompt: 'Synthesize:' })
252
- .build())
253
- .step('respond',
254
- Conditional.create() // conditional: one branch runs
255
- .when('urgent', (i) => i.message.startsWith('URGENT'), urgentAgent)
256
- .otherwise('normal', normalAgent)
257
- .build())
258
- .build();
259
-
260
- await pipeline.run({ message: 'URGENT: refund dispute on order #4411' });
261
- ```
262
-
263
- The fourth primitive is `Loop` — `Loop.repeat(agent).until(guard).times(5)`, with a mandatory budget guard. And the named patterns from the research literature ship pre-composed from the same four: `selfConsistency` · `reflection` · `debate` · `mapReduce` · `tot` · `swarm` · `llmSwarm` (a swarm whose hand-offs an LLM decides). Because every composition is a flowchart, the structure you wrote is the structure you see in the UI — and the trace spans the whole pipeline, not one agent at a time. [Designing systems of agents ↓](#-build--design-your-agent-or-system-of-agents)
264
-
265
- ---
108
+ **No cloud account?** `ollama('llama3.2')` from `agentfootprint/providers` runs the same agent against a local model for $0 — the free rung between the mock and the bill. Full recipes: [Ollama](https://agentfootprint.dev/docs/build/ollama/) · [OpenAI-compatible endpoints](https://agentfootprint.dev/docs/build/openai/#openai-compatible-endpoints-ollama-llamacpp-vllm-together-groq-lm-studio).
266
109
 
267
110
  ## How — we abstract context engineering
268
111
 
@@ -276,153 +119,9 @@ Skills, steering, RAG, facts, memory, guardrails — every name for context does
276
119
  </picture>
277
120
  </p>
278
121
  <p align="center">
279
- <sub><em>One primitive: <code>Injection = slot × trigger × cache</code>. Because the framework owns this point, every piece of context is <b>born tracked</b> — observability isn't wired up, it's a consequence of the abstraction. <a href="#the-model--what-we-abstract">The full model ↓</a></em></sub>
280
- </p>
281
-
282
- ## What tracking buys you
283
-
284
- **See it in 30 seconds** — four questions logs can't answer, each answered by code in this repo from a real run:
285
-
286
- ```text
287
- Q: Why did the model pick refund_full instead of refund_partial?
288
- A: margin 0.02 — ⚠ NARROW: the two tool descriptions read nearly identical
289
- (toolChoiceRecorder — and the catalog lint flags the pair before you ever run)
290
-
291
- Q: Why was this loan declined?
292
- A: decision ← [control: "DTI above the 0.43 affordability ceiling"] ← dti 0.52 ← monthlyDebt / income
293
- (decide() evidence + the causal slice — every hop is a real recorded edge)
294
-
295
- Q: Which piece of context made the answer wrong?
296
- A: CAUSAL: ablating fact 'vip-override' flipped the outcome in 3/3 seeded reruns
297
- (localizeContextBug — ranked proxies, counterfactual proof)
298
-
299
- Q: Prove nobody edited this run's record.
300
- A: verifyAuditBundle → valid: false, brokenAt: #16 — the tampered record, named
301
- (hash-chained audit export, offline verification)
302
- ```
303
-
304
- And you don't have to read the trace yourself — the trace toolpack lets a debugger model do it: in one run it found a planted bug while reading **9.5% of the trace** ([guide](docs/guides/trace-debugging.md)).
305
-
306
- And the watching costs the run nothing:
307
-
308
- <p align="center">
309
- <picture>
310
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/event-loop-dark.svg">
311
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/event-loop-light.svg">
312
- <img alt="Your agent is the event loop — animated. Left: your agent code (Context, Call LLM, Tool Calls) looping turn after turn. Right: the JS event loop drawn as two bold curved arrows with a traveling cursor and two stops — the call stack, where each stage runs as a frame and feeds four trace events (structure, data, control, emit) into the trace queue at the loop's center; and idle time, where the dispatcher flies the queued events into TRACE MEMORY and every listener (onStageAdded, onCommit, onDecision, onEmit) receives every event, one beat behind. Grey is JavaScript's own machinery, green is footprintjs, colors are your code and its trace." src="docs/assets/event-loop-light.svg" width="100%"/>
313
- </picture>
314
- </p>
315
- <p align="center">
316
- <sub><em>Your agent <b>is</b> the event loop: each stage feeds its trace events into a queue on the call stack; the dispatcher delivers them to your listeners in the idle beat — <b>one beat behind</b>, never on the hot path.</em></sub>
317
- </p>
318
-
319
- ## One contextual error, walked end to end
320
-
321
- The third question above, in full — every value below is the captured output of
322
- [`examples/observability/05-context-bisect.ts`](examples/observability/05-context-bisect.ts)
323
- and [`06-backtrack-trace.ts`](examples/observability/06-backtrack-trace.ts), runnable offline.
324
-
325
- **The bug.** A refunds agent carries a poisoned customer-profile fact. It answers:
326
-
327
- > *"Refund APPROVED: Dana Reyes holds VIP tier override status, so the 47-day-old
328
- > order qualifies for a refund beyond the 30-day window."*
329
-
330
- The policy says 30 days. The logs look fine — the model was *given* bad context,
331
- and classical logging has no row for that.
332
-
333
- **The walk.** Because context here is state, the decision backtracks like a
334
- variable: who read it, who wrote it, who let it in, where it was born —
335
-
336
- ```text
337
- ANSWER "Refund APPROVED…" ← the bug
338
- READ call-llm#40 assembled the system prompt ← exactly what the model saw
339
- LANDED context#6 wrote systemPromptInjections ← who mutated state
340
- ALLOWED trigger { kind: 'always' } — active every iteration ← why it was let in
341
- BORN defineFact('vip-override-fact') ← who wrote it
342
- ```
343
-
344
- That chain is the **provable candidate set** — everything that demonstrably reached
345
- the call, nothing else. Influence scoring then *ranks* inside it, and counterfactual
346
- **ablation proves**: removing `vip-override-fact` flips APPROVED → DECLINED in
347
- **3/3 seeded reruns** (the benign fact and the lookup tool: 0/3). Scores are proxies;
348
- only ablation makes the causal claim.
349
-
350
- **Three interfaces, one per shape of bug** — ship-a-default, bring-your-own:
351
-
352
- | interface | finds the culprit when it is… | confirm by |
353
- |---|---|---|
354
- | **influence ranking** (`scoreInfluence` + `rankingConfidence`) | **present** — orders suspects, says when it can't rank | — |
355
- | **ablation** (`localizeContextBug`) | **present** — *remove* it, see the outcome flip | removal |
356
- | **missing-context finder** (`findDroppedContext`) | **absent** — available but never reached the model (`available − sent`) | restoration |
357
-
358
- The third closes a gap the first two are blind to: a key instruction truncated out of
359
- the window has nothing to ablate, so you confirm by *restoration*. And when scoring is
360
- too flat to trust, `rankingConfidence` returns a shortlist to confirm rather than a
361
- confident, wrong #1. Guides: [ranking-confidence](docs/guides/ranking-confidence.md) ·
362
- [missing-context](docs/guides/missing-context.md).
363
-
364
- **The same walk, visual.** `toBacktrackTrace()` serializes the report into
365
- [Story Lens](https://github.com/footprintjs/agentThinkingUI)'s `<BacktrackView>`
366
- — the "why?" board, triggerable from any decision point (final answer, a mid-loop tool
367
- choice, a deterministic `decide()` rule):
368
-
369
- <img alt="The BacktrackView board: the wrong answer, the suspects with influence meters, the CAUSAL 3/3 ablation stamp on the planted fact, and the chain-of-custody rewind showing the exact system prompt the model saw with the culprit sentence highlighted." src="docs/assets/backtrack-board.png" width="100%"/>
370
- <p align="center">
371
- <sub><em>The wrong answer, suspects with influence meters, the <b>CAUSAL 3/3</b> ablation stamp on the planted fact, and the rewind showing the <b>exact system prompt the model saw</b> with the culprit highlighted — recorded state, not a reconstruction. Every id is a <code>runtimeStageId</code> a debugger LLM can drill via the <a href="docs/guides/trace-debugging.md">trace toolpack</a>. <a href="https://footprintjs.github.io/agentThinkingUI/demo/backtrack.html"><b>▶ Try it live</b></a> · run <a href="examples/observability/06-backtrack-trace.ts"><code>06-backtrack-trace.ts</code></a> offline.</em></sub>
122
+ <sub><em>One primitive: <code>Injection = slot × trigger × cache</code>. Because the framework owns this point, every piece of context is <b>born tracked</b> — observability isn't wired up, it's a consequence of the abstraction. <a href="#the-4-triggers">The 4 triggers ↓</a></em></sub>
372
123
  </p>
373
124
 
374
- ### Pick how influence is scored — then re-run without the noisy source
375
-
376
- Influence ranking is a pluggable strategy. Two ship out of the box:
377
- `semantic-alignment` (embeddings, the default) and `lexical-overlap` (plain word
378
- overlap — free and deterministic). `listInfluenceStrategies()` gives a UI everything
379
- it needs to offer a picker, and to grey out what it can't run.
380
-
381
- Found the source you suspect? `rerunWithoutSources` runs the same scenario again without
382
- it. It reports what changed — the answer, how often it flipped across seeded re-runs, and
383
- (with `checkBaseline`) a causal verdict. Scores suggest; re-runs convict.
384
-
385
- ```ts
386
- const again = await rerunWithoutSources({ report, ignore: ['social-sentiment'], runner, originalAnswer, embedder });
387
- again.answer; // 'HOLD …'
388
- again.whatChanged.summary; // 'Removing sources [social-sentiment] changed the answer in 3/3 seeded re-runs …'
389
- ```
390
-
391
- ### Chat sessions that can explain themselves
392
-
393
- `recordedChat` wraps your agent factory into a recorded conversation. Every `send()`
394
- freezes that turn's evidence. `reason(k)` shows what influenced reply K. `rerunTurn(k,
395
- { ignore })` re-runs that exact turn — same recorded history, byte for byte — without a
396
- source, and returns the same honest result `rerunWithoutSources` gives you. `fork(k,
397
- { fromRerun })` continues the conversation from the what-if as a new recorded session.
398
- Branch, never rewrite.
399
-
400
- ```ts
401
- import { recordedChat } from 'agentfootprint/observe';
402
-
403
- const chat = recordedChat({ makeAgent }); // your factory, specs applied at construction
404
- await chat.send('Should we BUY or HOLD?'); // recorded turn (frozen evidence)
405
- const rerun = await chat.rerunTurn(1, { // that turn, minus one source
406
- ignore: ['social-sentiment'], embedder, checkBaseline: true,
407
- });
408
- const fork = chat.fork(1, { fromRerun: rerun }); // continue from the what-if
409
- ```
410
-
411
- ---
412
-
413
- ## Pick your door
414
-
415
- | 🔧 Building an agent? | 🐛 Agent misbehaving? | 🏛️ Need audit / compliance? |
416
- |---|---|---|
417
- | Typed agents with skills, steering, RAG, memory, guardrails — and the trace for free. | Lint your tool catalog in 5 minutes — works on **any** framework's tool list (plain JSON / MCP / OpenAI / Anthropic shapes). Then causal slices, context bisection, and the debugger-LLM toolpack. | Hash-chained, tamper-evident run records with an offline verifier — record-keeping in the EU-AI-Act shape. |
418
- | [→ Quick start](#quick-start--runs-offline-no-api-key) · [→ Build ↓](#-build--design-your-agent-or-system-of-agents) | [→ Debug ↓](#-debug--see-what-your-agent-did) · [→ Tool-catalog lint](docs/guides/tool-catalog-lint.md) · [→ Trace debugging](docs/guides/trace-debugging.md) | [→ Audit ↓](#-audit--prove-what-happened) · [→ Security guide](docs/guides/security.md) |
419
-
420
- ---
421
-
422
- ## The model — what we abstract
423
-
424
- You collect domain-specific data and instructions — **Skills · Steering · Guardrails · RAG · Tool APIs · Memory**, with more on the way. They all do one thing: **inject into one of three slots** (`system`, `messages`, `tools`). So we abstracted the injection itself.
425
-
426
125
  <p align="center">
427
126
  <picture>
428
127
  <source media="(prefers-color-scheme: dark)" srcset="docs/assets/triggers-dark.svg">
@@ -452,429 +151,38 @@ That's the whole model: `Injection = slot × trigger × cache`.
452
151
  | `on-tool-return` | runtime — lifecycle | After a specific tool returns | `.instruction(defineInstruction({ id, activeWhen: s => s.lastToolResult?.toolName === 'search', prompt: 'Cite source IDs.' }))` | `system` |
453
152
  | `llm-activated` | runtime — agent-driven | LLM calls `read_skill('id')` | `.skill(defineSkill({ id: 'refund-policy', description, body }))` | `system` (body) + `tools` |
454
153
 
455
- > [!NOTE]
456
- > The "Illustration" column shows the shape of each flavor — the typed builder methods (`.steering` / `.instruction` / `.skill` / `.fact` / `.rag`) take an `Injection` (or `MemoryDefinition` for `.rag`) produced by the matching `defineSteering` / `defineInstruction` / `defineSkill` / `defineFact` / `defineRAG` factory. A `Skill` targets more than one slot at once: `tools` (the schemas it contributes — registered up front and visible from iteration 1 unless the Skill sets `autoActivate: 'currentSkill'`, which scopes them to the iterations where it is active) and `system` (its body — or, with `surfaceMode: 'tool-only'`, the `read_skill` result instead). The `messages` slot both projects the conversation and accepts delivery: `slot: 'messages'` (with a `role` you name) appends to the window itself, subject to what the attached provider carries inside `messages` and to a sequence rule that defers rather than reorders.
457
-
458
154
  **3 slots × 4 triggers × N flavors = the entire context-engineering surface.**
459
155
 
460
- ---
461
-
462
- ## Why we chose this abstraction
463
-
464
- The agent space has many credible primary abstractions:
465
-
466
- | Framework | What it abstracts |
467
- |---|---|
468
- | **LangChain** | Pipelines of composable components |
469
- | **LangGraph** | State machines of nodes and edges |
470
- | **CrewAI · AutoGen** | Crews of role-playing agents |
471
- | **Mastra · Genkit · Pydantic AI** | Typed full-stack bundles |
472
- | **DSPy** | Compiled prompts |
473
- | **Inngest AgentKit** | Durable workflows |
474
-
475
- We didn't have to choose between them.
476
-
477
- agentfootprint is built on **footprintjs** — the flowchart pattern for backend code. footprintjs gives us every one of those abstractions out of the box:
478
-
479
- | Capability | What footprintjs hands us |
480
- |---|---|
481
- | Composition | `Sequence` · `Parallel` · `Conditional` · `Loop` |
482
- | State machines | The ReAct loop *is* a flowchart |
483
- | Multi-agent crews | Compose Agents through control flow — no special class needed |
484
- | Durable workflows | `pauseHere()` plus JSON-portable `resume()` |
485
- | Typed observation | 60+ events for free, because the framework owns the loop |
486
-
487
- So we used the budget those abstractions would have cost us to invest deeply in something they all leave to the developer: **the injection loop.**
488
-
489
- > [!IMPORTANT]
490
- > **We abstract context engineering — and hand back the trace.**
491
- > Live to develop · offline to monitor · detailed to improve.
492
-
493
- ---
494
-
495
- ## 🔧 Build — design your agent or system of agents
496
-
497
- Two scales — same alphabet. Four control flows are the entire vocabulary.
498
-
499
- <table>
500
- <tr>
501
- <td width="50%" align="center">
502
- <picture>
503
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/sequence-dark.svg">
504
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/sequence-light.svg">
505
- <img alt="Sequence — linear chain A → B → C." src="docs/assets/sequence-light.svg" width="100%"/>
506
- </picture>
507
- </td>
508
- <td width="50%">
509
-
510
- ```typescript
511
- import { Sequence } from 'agentfootprint';
512
-
513
- const flow = Sequence.create()
514
- .step('a', stageA)
515
- .step('b', stageB)
516
- .step('c', stageC)
517
- .build();
518
- ```
519
-
520
- </td>
521
- </tr>
522
- <tr>
523
- <td width="50%" align="center">
524
- <picture>
525
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/parallel-dark.svg">
526
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/parallel-light.svg">
527
- <img alt="Parallel — fan-out then fan-in across N agents." src="docs/assets/parallel-light.svg" width="100%"/>
528
- </picture>
529
- </td>
530
- <td width="50%">
531
-
532
- ```typescript
533
- import { Parallel } from 'agentfootprint';
534
-
535
- const fan = Parallel.create()
536
- .branch('web', searchWeb)
537
- .branch('docs', searchDocs)
538
- .mergeWithFn(synthesizer)
539
- .build();
540
- ```
541
-
542
- </td>
543
- </tr>
544
- <tr>
545
- <td width="50%" align="center">
546
- <picture>
547
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/conditional-dark.svg">
548
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/conditional-light.svg">
549
- <img alt="Conditional — diamond gate routes to one of N branches based on a predicate." src="docs/assets/conditional-light.svg" width="100%"/>
550
- </picture>
551
- </td>
552
- <td width="50%">
553
-
554
- ```typescript
555
- import { Conditional } from 'agentfootprint';
556
-
557
- const router = Conditional.create()
558
- .when('billing', s => /bill|invoice|refund/.test(s.message), billingAgent)
559
- .when('tech', s => /error|bug|crash/.test(s.message), techAgent)
560
- .otherwise('default', defaultAgent)
561
- .build();
562
- ```
563
-
564
- </td>
565
- </tr>
566
- <tr>
567
- <td width="50%" align="center">
568
- <picture>
569
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/loop-dark.svg">
570
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/loop-light.svg">
571
- <img alt="Loop — body cycles back from end to start until a condition is met." src="docs/assets/loop-light.svg" width="100%"/>
572
- </picture>
573
- </td>
574
- <td width="50%">
575
-
576
- ```typescript
577
- import { Loop } from 'agentfootprint';
578
-
579
- const reflexion = Loop.create()
580
- .repeat(thinkAgent)
581
- .until(({ latestOutput }) => latestOutput.includes('DONE'))
582
- .build();
583
- ```
584
-
585
- </td>
586
- </tr>
587
- </table>
588
-
589
- ### Inside one agent — Dynamic vs Classic ReAct
590
-
591
- <p align="center">
592
- <picture>
593
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dynamic-vs-classic-dark.svg">
594
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/dynamic-vs-classic-light.svg">
595
- <img alt="Classic ReAct vs Dynamic ReAct loop topology — same 5 stages (SystemPrompt, Messages, Tools, CallLLM, Route → ExecuteTools/Finalize), but the loop edge differs: Classic returns to CallLLM only (slots frozen at 12 tools every iteration), Dynamic returns to SystemPrompt (slots recompose, tools shrink from 1 to 5 as skills activate)." src="docs/assets/dynamic-vs-classic-light.svg" width="100%"/>
596
- </picture>
597
- </p>
598
- <p align="center">
599
- <sub><em>Same five stages; only the loop edge differs. Classic returns to <code>CallLLM</code> with slots frozen; Dynamic (agentfootprint) returns to <code>SystemPrompt</code>, so injections recompose the next prompt — also the prerequisite for per-iteration caching.</em></sub>
600
- </p>
601
-
602
- | Iteration | Classic ReAct | Dynamic ReAct (agentfootprint) |
603
- |---|---|---|
604
- | 1 | 12 tools shown | **1 tool** (`read_skill`) |
605
- | 2 | 12 tools shown | **5 tools** (skill activated) |
606
- | 3 | 12 tools shown | 5 tools |
607
-
608
- > 📖 [Dynamic ReAct guide](https://agentfootprint.dev/docs/build/dynamic-react/) · [Key concepts](https://agentfootprint.dev/docs/getting-started/key-concepts/)
609
-
610
- ### Multi-agent — compose with the alphabet
611
-
612
- <p align="center">
613
- <picture>
614
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/compose-dark.svg">
615
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/compose-light.svg">
616
- <img alt="A custom research agent built from the same 4 control flows: input flows into a Conditional gate (plan more research?), which fans out to a Parallel block (search_web, search_docs, search_kb), then chains into a Sequence (synthesize → critique), and a Loop arrow returns from the end back to the Conditional gate so the agent iterates until satisfied. Formula: Loop( Conditional(plan?) → Parallel(search_web, search_docs, search_kb) → Sequence(synth → critique) )." src="docs/assets/compose-light.svg" width="100%"/>
617
- </picture>
618
- </p>
619
-
620
- Pick the flows that match your problem. Chain them. **That's your Agentic Application.**
621
-
622
- ```typescript
623
- const research = Loop.create()
624
- .repeat(Sequence.create().step('plan', plan).step('search', searchAll).build())
625
- .until(({ iteration, latestOutput }) => iteration >= 3 || latestOutput.includes('DONE'))
626
- .build();
627
- ```
628
-
629
- ### Named patterns — also compositions of the same 4
630
-
631
- <p align="center">
632
- <picture>
633
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/patterns-dark.svg">
634
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/patterns-light.svg">
635
- <img alt="6 named multi-agent patterns reduce to compositions of the same 4 control flows: Swarm = Loop(Parallel(Agent×N) → merge); Tree-of-Thoughts = Loop(Parallel(Agent×N) → Conditional(score)); Reflexion = Loop(Agent → Conditional(critique) → Agent); Debate = Parallel(Agent_pro, Agent_con) → Agent_judge; Router = Conditional → Agent_A | Agent_B | Agent_C; Hierarchical = Agent_planner → Sequence(Agent_worker×N) → synth." src="docs/assets/patterns-light.svg" width="100%"/>
636
- </picture>
637
- </p>
638
-
639
- The patterns the field knows reduce to the same alphabet:
640
-
641
- | Pattern | Composition |
642
- |---|---|
643
- | **Swarm** | `Loop( Parallel( Agent×N ) → merge )` |
644
- | **Tree-of-Thoughts** | `Loop( Parallel( Agent×N ) → Conditional(score) )` |
645
- | **Reflexion** | `Loop( Agent → Conditional(critique) → Agent )` |
646
- | **Debate** | `Parallel( Agent_pro, Agent_con ) → Agent_judge` |
647
- | **Router** | `Conditional → Agent_A \| Agent_B \| Agent_C` |
648
- | **Hierarchical** | `Agent_planner → Sequence( Agent_worker×N ) → synth` |
649
-
650
- Same trick as the injection model: instead of N libraries for N patterns, we found the M building blocks all N patterns are made of.
651
-
652
- > 📖 Compare: [hand-rolled vs declarative](https://agentfootprint.dev/docs/getting-started/why/) · [migration from LangChain / CrewAI / LangGraph](https://agentfootprint.dev/docs/getting-started/vs/)
653
-
654
- ### Check in with the receipts — human-in-the-loop consent for consequential actions
655
-
656
- OpenWorker-class agents check in; agentfootprint checks in **with the receipts.** A tool declares `checkIn: 'always'` (or a `(args) => boolean` predicate). When it trips, the run pauses **before** the tool executes and hands back an evidence pack: `willDo` (plain-words claim), `read` (context the run consumed), `drivers` (which context drove the choice, ranked, zero LLM calls), and a compact `trail`. A human answers `checkInApproved({ by })` / `checkInDeclined({ by, note })`; on approve the tool runs, on decline the model sees the note and adapts.
657
-
658
- ```ts
659
- const refund = defineTool({
660
- name: 'issue_refund', description: 'Issue a refund',
661
- inputSchema: { type: 'object', properties: { amount: { type: 'number' } } },
662
- checkIn: (args) => args.amount > 1000, // ask a human only for big refunds
663
- execute: ({ amount }) => `refunded ${amount}`,
664
- });
665
- const out = await agent.run({ message: 'refund 5000' });
666
- if (isCheckInPause(out)) { // distinct from a plain askHuman pause
667
- showToHuman(out.checkIn.evidence); // the receipts
668
- await agent.resume(out.checkpoint, checkInApproved({ by: 'alice@ops' }));
669
- }
670
- ```
671
-
672
- It rides the same JSON checkpoint as pause/resume — the ask and the decision can be servers and days apart — and lands `checkin.request` / `checkin.decision` as typed events (`CheckInRecorder` captures the audit trail). A tool without `checkIn` is byte-identical. Permission (policy) still runs first — check-in is **consent**, not policy.
673
-
674
- Run the flagship demo — an AI coworker that drafts a weekly status doc and checks in before posting it (deterministic $0 mock, watch it adapt on decline):
675
-
676
- ```bash
677
- npm run example examples/features/34-checkin-coworker.ts -- --decline
678
- ```
679
-
680
- See the [Check-in guide](https://agentfootprint.dev/docs/monitor/checkin/).
681
-
682
- ### Act — everything your agent does about its own loop, in one block
683
-
684
- **Tools do the work. Act decides about the work. Watch remembers both — and nothing can act without being watched.**
685
-
686
- <p align="center">
687
- <picture>
688
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/loop-moments-dark.svg">
689
- <source media="(prefers-color-scheme: light)" srcset="docs/assets/loop-moments-light.svg">
690
- <img alt="One agent turn drawn as a circle. The message enters through the INPUT gate; the loop runs clockwise past the WINDOW gate (what the live context keeps), the context slots and the model call, the BEFORE-TOOL gate (every call, before dispatch), the tool actually running, the AFTER-TOOL gate (every result, before the model reads it) and the end of the iteration; the answer leaves through the OUTPUT gate. Five amber gates are the moments .act() can change; purple watch-dots sit at every moment, including the ones with no gate, because watch attends all of them and cannot be switched off." src="docs/assets/loop-moments-light.svg" width="100%"/>
691
- </picture>
692
- </p>
693
-
694
- ```ts
695
- const agent = Agent.create({ provider, model })
696
- .act({
697
- input: [scrubSSNs], // the message, before the run commits it
698
- beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
699
- afterTool: [stripPII], // every result, before the model reads it
700
- window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
701
- output: [noCodenames], // the answer, before the caller gets it
702
- })
703
- .build();
704
- ```
705
-
706
- Five keys, one per moment, in the order the loop reaches them — so autocomplete on an empty `{}` teaches the loop. Every rule answers `allow()`, `allow(value, why)` or `deny(reason)` (and `ask({ question })` where a person can still change the outcome), **and none of them can answer for the tool**: the outcome union has no result arm, so what the model finally reads is the real tool's output or a refusal. Every decision files a ledger row stamped with its `moment`.
707
-
708
- It is pure sugar over the five individual doors, pinned byte-equivalent per key — and the bundle's keys are locked against `LoopMoment` at compile time, so a sixth moment cannot ship without a key for it.
709
-
710
- ```bash
711
- npm run example examples/features/38-act.ts
712
- ```
713
-
714
- See [The moments of the loop](https://agentfootprint.dev/docs/build/loop-moments/).
715
-
716
- ### Watch — who is looking while it does
156
+ ## What tracking buys you
717
157
 
718
- `.act()` says what the agent may do. `.watch()` says who is looking while it does it. Observers handed to the builder are attached before `build()` returns, so there is no window where the agent has run and nobody was watching:
158
+ **See it in 30 seconds** — four questions logs can't answer, each answered by code in this repo from a real run:
719
159
 
720
- ```ts
721
- const routes = routeRecorder();
722
- const choices = toolChoiceRecorder({ embedder: staticEmbedder() });
160
+ ```text
161
+ Q: Why did the model pick refund_full instead of refund_partial?
162
+ A: margin 0.02 — ⚠ NARROW: the two tool descriptions read nearly identical
163
+ (toolChoiceRecorder — and the catalog lint flags the pair before you ever run)
723
164
 
724
- const agent = Agent.create({ provider, model })
725
- .watch(routes, choices) // build-time — sees the very first run
726
- .act({ beforeTool: [refundCeiling] })
727
- .build();
165
+ Q: Why was this loan declined?
166
+ A: decision ← [control: "DTI above the 0.43 affordability ceiling"] ← dti 0.52 ← monthlyDebt / income
167
+ (decide() evidence + the causal slice — every hop is a real recorded edge)
728
168
 
729
- await agent.run({ message: 'refund order 4471' });
169
+ Q: Which piece of context made the answer wrong?
170
+ A: CAUSAL: ablating fact 'vip-override' flipped the outcome in 3/3 seeded reruns
171
+ (localizeContextBug — ranked proxies, counterfactual proof)
730
172
 
731
- console.log(await choices.getFlagged()); // calls where the tool choice was a near-tie
173
+ Q: Prove nobody edited this run's record.
174
+ A: verifyAuditBundle → valid: false, brokenAt: #16 — the tampered record, named
175
+ (hash-chained audit export, offline verification)
732
176
  ```
733
177
 
734
- Variadic, because observers come in sets. It returns the builder — the runtime door is still `agent.attach(observer)`, which returns an `Unsubscribe` you own and call when the observer's life ends. Same mechanism underneath, so mixing them is fine and order is preserved.
735
-
736
- There is deliberately no list of "watch moments" to go with `.act()`'s five. A rule has to be *told* where it may speak, so that list is closed and compiler-pinned; an observer attends the whole stream, and any list we published would be a vocabulary we then had to keep true.
737
-
738
- *(`.recorder()` was the same door under its old, internals-flavoured name. It was removed in 9.0.0 and now throws a sentence pointing here — see [9.0.0](./CHANGELOG.md).)*
739
-
740
- ---
741
-
742
- ## 🐛 Debug — see what your agent did
743
-
744
- <p align="center">
745
- <img src="docs/assets/lens-run.png" alt="A real agent run in Why Lens: the conversation (with live PII redaction), the executed path lit on the merge-tree flowchart, the WHAT-HAPPENED timeline of every iteration/context/LLM turn/route, run stats, and the step inspector — all generated from the run's own trace." width="100%">
746
- </p>
747
- <p align="center">
748
- <sub>One real run, fully explained — <a href="https://github.com/footprintjs/agentfootprint-lens"><b>Why Lens</b></a> (<code>npm i agentfootprint-lens</code>): conversation · executed path · per-step timeline · stats, every pixel from the trace.</sub>
749
- </p>
750
-
751
- Because we own the loop, every decision and execution is captured during traversal — not bolted on. The default capture is the **causal trace**: every stage, read, write, and decision evidence as a JSON-portable, scrubbable, queryable, exportable artifact — and every LLM call backtracks to four typed answers: **what** was injected, **who** triggered it (which rule), **when** it fired, **how** it landed (slot · position · cache). Beyond the default, wire custom recorders for cost, latency, or quality scoring — any observation hook fires on the same stream.
752
-
753
- The same trace serves three downstream consumers — no extra instrumentation:
178
+ And you don't have to read the trace yourself — the trace toolpack lets a debugger model do it: in one run it found a planted bug while reading **9.5% of the trace** ([self-explain](https://agentfootprint.dev/docs/debug/self-explain/)). The watching costs the run nothing: recorders run one beat behind the agent, never on its hot path.
754
179
 
755
- | Consumer | What the trace gives you |
756
- |---|---|
757
- | **Audit / compliance** | Six months later, *"why was loan #42 rejected?"* answers from the chain (`creditScore=580 < 620 ∧ dti=0.6 > 0.43 → riskTier=high → REJECTED`) — no LLM call. GDPR Art. 22 / ECOA / EU AI Act adverse-action notices write themselves from the captured decision evidence. |
758
- | **Cheap-model triage** | A Sonnet trace is good *input* for Haiku to answer follow-ups: ~200 tokens ($0.25/1M) vs ~2,500 at a reasoning model ($15/1M). Memoized thinking — no agent rerun. |
759
- | **Training data** | Every successful chain is a labeled trajectory; SFT pairs fall out of the snapshot's history field. (The export wrapper, DPO, and process-RL need extra collection layers — [roadmap](https://github.com/footprintjs/agentfootprint/issues).) |
760
-
761
- Four views, one trace — pick by question:
180
+ ## Pick your door
762
181
 
763
- | View | Shows | When to use |
182
+ | 🔧 Building an agent? | 🐛 Agent misbehaving? | 🏛️ Need audit / compliance? |
764
183
  |---|---|---|
765
- | **Story Lens** (the hero up top) | The run replayed as an animated, scrubbable story — the brain, the tools, the reasoning | Show anyone *what the agent did* |
766
- | **BacktrackView** ([the board above](#one-contextual-error-walked-end-to-end)) | A decision walked backwards — suspects, influence meters, ablation stamps, custody rewind | Answer *why it decided that* |
767
- | **Why Lens** | Agent-centric — User/Agent[3 slots]/Tool flowchart with iteration scrubber and round commentary | Live debugging, "what did the agent see at step 5?" |
768
- | **Explainable Trace** | Structural — subflow tree, full flowchart, memory inspector, per-stage execution timeline | Architecture review, root-cause analysis |
769
-
770
- And two **conversational** doors over the same evidence — ask instead of look:
771
-
772
- ```ts
773
- // dedicated: a cheap model debugs an expensive run by id — pays for what it opens
774
- const debuggerAi = traceDebugAgent({ artifacts, provider: anthropic(), model: 'claude-haiku-4-5' });
775
- await debuggerAi.run({ message: 'Why was loan APP-7 approved?' });
776
-
777
- // in-conversation: the agent answers "why did you…?" from its OWN previous turn
778
- Agent.create({ provider, model }).tool(lookupOrder)
779
- .selfExplain({ delegate: { provider: anthropic(), model: 'claude-haiku-4-5' } })
780
- .build();
781
- ```
782
-
783
- `.selfExplain()` mounts one skill: the catalog stays clean until the LLM activates
784
- it, evidence binds only to **completed** runs (never in-flight), and `delegate`
785
- answers at the cheap model's price inside the expensive conversation.
786
- [Guide](docs/guides/trace-debugging.md) · examples
787
- [`07`](examples/observability/07-trace-debug-agent.ts) ·
788
- [`08`](examples/observability/08-self-explain.ts) · the doors walk the
789
- [**same evidence the board visualizes ▶**](https://footprintjs.github.io/agentThinkingUI/demo/backtrack.html).
790
-
791
- > 📖 Powered by [footprintjs `causalChain()`](https://footprintjs.github.io/footPrint/blog/backward-causal-chain/) — backward thin-slicing on the commit log. [Causal memory deep dive](https://agentfootprint.dev/docs/debug/causal-deep-dive/) · [Explainability & compliance](https://footprintjs.github.io/footPrint/blog/explainability-compliance/)
792
-
793
- **One recording. Two lenses. Three consumers. Zero extra instrumentation.**
794
-
795
- ### Observers stay off the hot path
796
-
797
- By default every `agent.on()` listener runs synchronously inside the producing
798
- statement. One option moves observation off the hot path:
799
-
800
- ```ts
801
- Agent.create({ provider, model, observerDelivery: 'deferred' }) // default 'inline'
802
- // serverless / shutdown: settle async listener work before the freeze
803
- await agent.drainObservers({ timeoutMs: 5_000 });
804
- ```
805
-
806
- Events are captured into a bounded queue (≈ microseconds on the hot path) and
807
- delivered one beat behind — same typed events, same order, zero loss, a throwing
808
- listener can't kill the run, and per-listener stats land on
809
- `getLastSnapshot()?.observerStats` to name the hog. Terminal boundaries (resolve,
810
- crash, pause) drain synchronously first, so checkpoints are always complete.
811
- Measured: −8% wall on a 50-iteration agent with a deliberately slow listener
812
- ([example 21](examples/features/21-deferred-observers.ts)).
813
-
814
- > 📖 Full semantics (capture policies, backpressure, overflow):
815
- > [deferred-observers guide](https://github.com/footprintjs/footPrint/blob/main/docs/guides/observers-deferred.md)
816
-
817
- ### Lint your tool catalog — before the model picks the wrong twin
818
-
819
- Tool routing is an LLM decision driven by names + descriptions — so lint the
820
- catalog like code and gate it in CI. **Zero stack buy-in**: works on any
821
- OpenAI / Anthropic / MCP / plain tool list, no agentfootprint runtime needed.
822
-
823
- ```bash
824
- npx agentfootprint-lint-tools tools.json --threshold 0.94 --strict
825
- ```
826
-
827
- ```
828
- ✗ CONFUSABLE 0.9445 get_fcns_database <> influx_get_fcns_database
829
- hint: names differ only by 'influx' — make the descriptions say WHEN to choose each
830
- ~ warn [enum-in-prose] influx_get_port_ranking.metric
831
- suggest: "enum": ["avg_iops","peak_iops","mbps"]
832
- ```
833
-
834
- Pairwise confusability over what the model reads (embedder pluggable,
835
- content-hash cached) plus a pluggable structural rule pack
836
- (missing/short descriptions, says-WHAT-not-WHEN, enums hiding in prose,
837
- undocumented optional params). The runtime counterpart, `toolChoiceRecorder`
838
- (`agentfootprint/observe`), scores each live LLM call's tool choice against
839
- the same geometry and flags narrow margins and proxy disagreements — lazily,
840
- off the hot path.
841
-
842
- agentfootprint owns the *detection* — the scores, the ties, the recorded
843
- margins; you own the *policy* — the threshold, the embedder, the rewrite. We
844
- map; you decide.
845
-
846
- > 📖 **[Tool-catalog lint guide](docs/guides/tool-catalog-lint.md)** — 5 minutes
847
- > from a tools.json to a gated CI check ·
848
- > [`examples/observability/02`](examples/observability/02-lint-confusable-catalog.ts) ·
849
- > [`03`](examples/observability/03-lint-fix-and-pass.ts) ·
850
- > [`04`](examples/observability/04-tool-choice-margins.ts)
851
-
852
- ---
853
-
854
- ## 🏛️ Audit — prove what happened
855
-
856
- Answering *"why was the loan rejected?"* from captured evidence is the [debug door above](#-debug--see-what-your-agent-did). The audit door adds the integrity layer: prove the **record itself** hasn't been edited since capture. `auditExport()` hash-chains every typed event — decisions, tool calls, validation rejections, permission verdicts, costs — into an append-only bundle (EU AI Act Art. 12 record-keeping shape); `verifyAuditBundle()` re-checks it **offline** — no agent, no LLM — and names the exact record any tamper broke.
857
-
858
- ```ts
859
- import { auditExport, verifyAuditBundle } from 'agentfootprint/observe';
860
-
861
- const audit = auditExport({ agent: 'ledger-auditor' });
862
- const stop = agent.enable.observability({ strategy: audit });
863
- await agent.run({ message: 'audit account ACCT-1142' });
864
- stop();
865
-
866
- const bundle = audit.bundle(); // plain JSON — store anywhere
867
- verifyAuditBundle(bundle); // { valid: true, recordsChecked: 50 }
868
- // flip one byte anywhere → { valid: false, brokenAt: 13, reason: 'hash mismatch — …' }
869
- ```
870
-
871
- Payloads are PII-bounded by default (tool args as key names, results as a type, content as `[N chars]` markers). And it's honest about its limits: tamper-**evident**, not tamper-proof — for non-repudiation, anchor both chain ends in external storage (WORM store, signed log).
872
-
873
- > 📖 **[Tamper-evident audit guide](docs/guides/security.md#tamper-evident-audit-export--auditexport--verifyauditbundle)** ·
874
- > [`examples/features/19-audit-export.ts`](examples/features/19-audit-export.ts) — capture → verify → tamper → drain ·
875
- > [`20-regulated-decisioning.ts`](examples/features/20-regulated-decisioning.ts) — an offline auditor reconstructs a loan decline from persisted files, both chain ends anchored
876
-
877
- ---
184
+ | Typed agents with skills, steering, RAG, memory, guardrails — and the trace for free. | Lint your tool catalog in 5 minutes — works on **any** framework's tool list. Then causal slices, context bisection, and the debugger-LLM toolpack. | Hash-chained, tamper-evident run records with an offline verifier — record-keeping in the EU-AI-Act shape. |
185
+ | [→ Quick start](https://agentfootprint.dev/docs/getting-started/quick-start/) · [→ Build an agent](https://agentfootprint.dev/docs/build/agent/) | [→ Debug](https://agentfootprint.dev/docs/debug/debug/) · [→ Tool-catalog lint](https://agentfootprint.dev/docs/debug/tool-catalog-lint/) · [→ Self-explain](https://agentfootprint.dev/docs/debug/self-explain/) | [→ Exporters & audit bundles](https://agentfootprint.dev/docs/monitor/exporters/) · [→ Security](https://agentfootprint.dev/docs/monitor/security/) |
878
186
 
879
187
  ## Mocks first, production second
880
188
 
@@ -889,71 +197,13 @@ Build the entire app against in-memory mocks with **zero API cost**, then swap r
889
197
 
890
198
  The flowchart, recorders, and tests don't change between dev and prod.
891
199
 
892
- ---
893
-
894
- ## What ships today
895
-
896
- <details>
897
- <summary><b>Full capability list</b> — core · 7 providers · memory + adapters · operability · debugging &amp; compliance · tooling</summary>
898
-
899
- <br/>
200
+ ## Building with an AI coding assistant
900
201
 
901
- **Core**
902
- - 2 primitives — `LLMCall`, `Agent` (the ReAct loop)
903
- - 4 control flows — `Sequence`, `Parallel`, `Conditional`, `Loop` (plus `workflow()`, the same sequence with every hand-off type-checked by the compiler, and `graph()`, a fixed DAG whose independent nodes run concurrently)
904
- - 1 Injection primitive — `defineSkill` / `defineSteering` / `defineInstruction` / `defineFact`
905
- - 1 reliability gate — `.reliability({ preCheck, postDecide, providers, circuitBreaker, fallback })`
906
- - 1 tool dispatch primitive — `ToolProvider` (sync OR async) — `staticTools` · `gatedTools` · `skillScopedTools` · or a custom `ToolProvider` that discovers over hubs / MCP / per-tenant catalogs
202
+ agentfootprint is written to be used — and maintained — by coding agents.
907
203
 
908
- **LLM providers** (8)
909
-
910
- | Factory | Use for |
911
- |---|---|
912
- | `anthropic` | Claude (Sonnet, Opus, Haiku) via `@anthropic-ai/sdk` |
913
- | `openai` | GPT-4o, GPT-4-turbo via `openai` SDK |
914
- | `bedrock` | Claude / Titan / Mistral via AWS Bedrock runtime |
915
- | `gemini` | Gemini via `@google/genai` — two doors, **Vertex** (project + ADC) or the Gemini API (one key). Not every door/model pair works: read the [door/model matrix](https://agentfootprint.dev/docs/infrastructure/google-cloud/#the-doormodel-matrix) first |
916
- | `ollama` | Local models, over Ollama's native API — no SDK, no key, real token counts, refusals that name `ollama serve` / `ollama pull` · `openai({ baseURL })` reaches llama.cpp, vLLM, and any other OpenAI-compatible endpoint |
917
- | `browserAnthropic` | Browser-side Claude calls (no proxy server) |
918
- | `browserOpenai` | Browser-side OpenAI calls (no proxy server) |
919
- | `mock` | Deterministic dev/test (zero API cost) |
920
-
921
- **Memory + adapters**
922
- - Memory factory — 4 types (`episodic` / `semantic` / `narrative` / `causal`) × 7 strategies (`window` / `budget` / `summarize` / `topK` / `extract` / `decay` / `hybrid`)
923
- - Memory stores — `InMemoryStore`, `RedisStore` (peer-dep `ioredis`), `AgentCoreStore` (peer-dep AWS SDK)
924
- - RAG · MCP adapters — `mockMcpClient(...)` / `mcpClient({ transport })`
925
-
926
- **Operability**
927
- - Provider-agnostic prompt caching — declarative per-injection, per-iteration marker recomputation
928
- - Artifacts (the claim check) — tools check data into a governed store (`ctx.artifacts`; in-memory / file / SQLite / S3 / Cloud Storage) and the model routes ~26-char tickets instead of hauling payloads: `wants` resolves ref arguments at dispatch with teaching refusals, the placement threshold (`artifacts: { store, placement }`) auto-refs oversized tool results, and the auto-attached `present` tool hands a ref to the screen with a durable description snapshot — every mint / resolve / refusal / presentation a typed event
929
- - Artifact stores everywhere the agent runs — one five-verb port, five adapters: `inMemoryArtifacts` · `fileArtifacts` · `sqliteArtifacts` · `s3Artifacts` · `gcsArtifacts`, all running the same contract suite. Scope-partitioned keys (a `..` tenant is a name, never a hop), the ticket as one metadata entry, digest verified on read, retention stated at mint. Optional `putStream` / `getStream` are **feature-detected** (`canStreamArtifacts`) — a store that cannot stream leaves them absent rather than faking it. The cloud adapters ship contract-shaped and tested, awaiting field use
930
- - Skill artifact vocabularies — a skill or a step declares `produces` / `consumes` (artifact kinds), and `graph.checkup()` warns `artifact-kind-unsatisfied` when nothing on the agent claims to make what a consumer needs. Honest by construction: a warning, never an error, because it reads declarations only and artifacts outlive the turn that made them
931
- - Human-in-the-loop pause / resume — a tool calls `pauseHere(...)` (or `askHuman(...)`); `isPaused(result)` hands you a JSON-serializable checkpoint, and `agent.resume(checkpoint, input)` continues hours later on a different server
932
- - Resilience primitives — `withRetry`, `withFallback`, `withCircuitBreaker`, `.outputFallback`, `agent.resumeOnError`
933
- - Context Integrity — deterministic checks at the seams where a run contradicts ITSELF: a tool parked but still on the wire, a tool offered after the results grounding it were evicted, an answer field that disagrees with the fact it claims to report (`.claims()`, requires `.outputSchema()`). Nothing is blocked or rewritten — each defect is one typed finding, and every run files a disposition ledger so "no findings" and "no check ran" stay different states. `integrityPosture: 'dev'` adds the liveness proofs (a start-of-run canary; `CheckerDeadError` instead of a green report from a checker that never ran). Read it back with `find_context_errors` over a recording — [Context Integrity](https://agentfootprint.dev/docs/monitor/context-integrity/)
934
- - Findings ledger — `.findings()` adds one reserved optional argument, `_findings`, to every served tool schema: the model declares why it makes each call (`basis`: `direct` or `exploratory`) and, on its next call or as a top-level key of a JSON answer, the standing of each earlier result by its tool_result id (`fact` with the assertions it stands on, `open`, `ruled-out`, `noise`). Zero extra model calls. The argument is peeled off before the tool, the middleware or the validator sees the call; the rows land as one append-only `findingsLedger` on the run's state (`agent.findings()`), with a `conflict` row whenever two stood-on readings disagree. Nothing is inferred — a call that declares nothing files nothing — and nothing is served differently yet: the ledger is a record. An agent that never calls `.findings()` is byte-identical to one built before it existed
935
- - Tool choice by classifier — `.toolChoice({ classifier, serve })` asks a calibrated classifier (`agentfootprint/classify`) at every model call which of the tools about to be served answers the current step, and files its ranking, its pick and its cost as a `pick` row beside the model's own call, then an `outcome` row after the reply (`firstAgrees`, any `miss`). The model's call is never overridden. `serve: { top: N }` narrows the served list to the classifier's top-N plus the doors (`read_skill`, `skip_step`, `present`, your `alwaysServe`), on the committed record the receipt hashes and `servedAt` rebuilds; a failed classifier serves the full set, a miss serves the full set on the next call. `npm run bench:tool-choice` measures agreement, misses and tools-slot bytes from the record. An agent without `.toolChoice()` is byte-identical
936
- - The ontology — `.ontology(map)` mounts a declared map of the domain (`defineOntology` on `agentfootprint/ontology`): what each term IS (meaning, unit), how terms RELATE, which SOURCE holds a term with the author's coverage sentence, and which registered TOOL reads it from there. A map, not a door — it provides no way to get data, nothing is fetched through it and the library infers nothing from it; it is on the record once as `AgentState.ontology`, served as one request-only system piece per call (every line the declaration's; a node nobody holds is `known, not held here`), hashed on the receipt and rebuilt by `servedAt`, and the model is asked to name the source or neighbouring node a need points at — as a proposal. `via` tool names are checked against the registry at build. An agent without `.ontology()` is byte-identical
937
- - 60+ typed observability events — `agent` · `composition` · `context` · `stream` · `tools` · `skill` · `memory` · `cache` · `cost` · `permission` · `eval` · `embedding` · `pause` · `error` · `fallback` · `resilience` · `reliability` · `risk`
938
-
939
- **Debugging & compliance** (`agentfootprint/observe`)
940
- - Tool-catalog lint — `npx agentfootprint-lint-tools` (any framework's tool list) + runtime `toolChoiceRecorder` margins
941
- - Contextual-bug localizer — `localizeContextBug` (causal slice → influence ranking → counterfactual ablation) + `bisectCulprits`
942
- - `toBacktrackTrace` — render any decision as the BacktrackView "why?" board
943
- - Trace toolpack — 11 bounded, LLM-callable tools so a debugger model walks the trace by id, including `find_context_errors` (what the run contradicted itself about, joined to the step that filed it) and `inspect_tool_run`: the descent INTO a tool call, when the tool kept its own record (`flowchartAsTool({ keepRecord: true })`)
944
- - `traceDebugAgent` (dedicated debugger session) · `.selfExplain()` (in-conversation why-questions, skill-gated, with a cheap-model `delegate` switch)
945
- - OTel GenAI span export · hash-chained tamper-evident audit bundles with an offline verifier
946
-
947
- **Tooling**
948
- - **Story Lens** — animated run player + BacktrackView why-board (separate `agentthinkingui` package)
949
- - **Why Lens** · **Explainable Trace** — two visual replays of the causal trace (separate `agentfootprint-lens` package)
950
- - AI-coding-tool support — Claude Code · Cursor · Windsurf · Cline · Kiro · Copilot
951
-
952
- </details>
953
-
954
- > 📖 [Agent API reference](https://agentfootprint.dev/docs/api/classes/Agent/) · [CHANGELOG](./CHANGELOG.md)
955
-
956
- ---
204
+ - **Teach your assistant the API:** `npx agentfootprint-setup` installs the agentfootprint skill for Claude Code (and rule files for Cursor, Copilot, Windsurf, Cline, Kiro). It loads only when the task is about agentfootprint.
205
+ - **Before building something, search [CAPABILITIES.md](CAPABILITIES.md)** — an index of what already ships, keyed by what *you* would call it. It is in the npm package too.
206
+ - **Every example runs:** [`examples/`](examples/) holds 160+ runnable programs, each executed end to end in CI against mock providers.
957
207
 
958
208
  ## Where to next
959
209
 
@@ -968,21 +218,14 @@ The flowchart, recorders, and tests don't change between dev and prod.
968
218
 
969
219
  Or jump into the [examples gallery](https://github.com/footprintjs/agentfootprint/tree/main/examples) — every example is also an end-to-end CI test.
970
220
 
971
- ---
972
-
973
221
  ## Tree-shakeable & ESM-first
974
222
 
975
- Import one thing, ship one thing. agentfootprint is built so your bundle grows only with what you actually use:
976
-
977
- - **Dual build, true ESM.** Ships CommonJS (`require`) **and** real ECMAScript Modules (`import`) with TypeScript types. The ESM build is `type:module` with explicit `.js` import extensions, so it loads as true ESM under Node, Vite, Next, Deno, and Bun — no shims.
978
- - **Per-file modules + honest `sideEffects`.** The dist is emitted file-by-file (never pre-bundled), so bundlers drop every export you don't touch. A small `import { defineTool }` doesn't pull in the Agent runtime, injection engine, memory stores, or LLM providers.
979
- - **Ten doors, named for what you're doing.** `agentfootprint` · `/providers` (plug in a backend) · `/memory` (state that outlives a turn) · `/observe` (everything that watches) · `/context` (how context gets assembled) · `/resilience` (when the call fails) · `/security` (who may do what) · `/hosting` (behind a wire) · `/events` (the typed wire vocabulary) · `/cache` (prompt caching). 8.0.0 consolidated 26 internals-named subpaths into these; every old path still resolves for all of 8.x.
980
- - **Lazy peer-deps.** Heavyweight integrations load their SDK **only when you instantiate them** — importing agentfootprint never bundles `@anthropic-ai/sdk`, `ioredis`, the AWS SDKs, or the MCP SDK unless you actually use that adapter.
981
- - **Optional families load when their option is enabled, never before.** What `import { Agent, defineTool }` costs is the library's *default graph*, not its whole surface. `.selfExplain()`'s trace toolpack — eleven tools over a finished trace, the largest single module in the package — is reached through a dynamic `import()` on the first iteration the self-explain skill is active; an agent that never enables it never ships it, and a bundler splits it into its own chunk. (A door that exists to hand you the debugger, `/observe`, carries it eagerly on purpose.)
223
+ - **Dual build, true ESM** — CommonJS and real ES modules with TypeScript types; loads natively under Node, Vite, Next, Deno and Bun.
224
+ - **Per-file modules, honest `sideEffects`** — `import { defineTool }` doesn't pull in the Agent runtime, memory stores or providers.
225
+ - **Entry points named for what you're doing** — `agentfootprint` plus `/providers`, `/memory`, `/rag`, `/observe`, `/context`, `/resilience`, `/security`, `/hosting`, `/events`, `/cache` and a few more; `package.json` `exports` is the complete list.
226
+ - **Lazy peer-deps** — vendor SDKs (Anthropic, AWS, Redis, MCP…) load only when you instantiate that adapter, and optional families like the self-explain toolpack load only when enabled.
982
227
 
983
- **Proven, not promised.** A CI smoke test bundles a minimal `import { defineTool }` and asserts the Agent runtime, injection engine, memory stores, and providers are pruned; a second test loads the main barrel and every subpath as true ESM and verifies the lazy-adapter loader works under ESM (`createRequire`, not a bare `require`). See [`test/esm-packaging.test.ts`](test/esm-packaging.test.ts). A third walks the code-split graph of the root entry and fails if the trace toolpack ever returns to its sync closure — [`test/lib/trace-toolpack/browserGraph.test.ts`](test/lib/trace-toolpack/browserGraph.test.ts); the docs site's own demo chunk is ratcheted against the same default graph in [`docs-next/scripts/check-site-budget.mjs`](docs-next/scripts/check-site-budget.mjs).
984
-
985
- ---
228
+ Proven by tests, not promised: [`test/esm-packaging.test.ts`](test/esm-packaging.test.ts) and [`test/lib/trace-toolpack/browserGraph.test.ts`](test/lib/trace-toolpack/browserGraph.test.ts).
986
229
 
987
230
  ## Built on
988
231