agentfootprint 7.0.0 → 7.2.0

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 (69) hide show
  1. package/CLAUDE.md +83 -622
  2. package/dist/core/agent/AgentBuilder.js +1 -1
  3. package/dist/core/agent/AgentBuilder.js.map +1 -1
  4. package/dist/debug.js +2 -1
  5. package/dist/debug.js.map +1 -1
  6. package/dist/esm/core/agent/AgentBuilder.js +1 -1
  7. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  8. package/dist/esm/debug.d.ts +1 -1
  9. package/dist/esm/debug.js +1 -1
  10. package/dist/esm/debug.js.map +1 -1
  11. package/dist/esm/lib/context-bisect/index.d.ts +1 -0
  12. package/dist/esm/lib/context-bisect/index.js +3 -0
  13. package/dist/esm/lib/context-bisect/index.js.map +1 -1
  14. package/dist/esm/lib/context-bisect/sliceToBacktrackTrace.d.ts +52 -0
  15. package/dist/esm/lib/context-bisect/sliceToBacktrackTrace.js +150 -0
  16. package/dist/esm/lib/context-bisect/sliceToBacktrackTrace.js.map +1 -0
  17. package/dist/esm/lib/context-ledger/contextLedger.d.ts +35 -0
  18. package/dist/esm/lib/context-ledger/contextLedger.js +309 -0
  19. package/dist/esm/lib/context-ledger/contextLedger.js.map +1 -0
  20. package/dist/esm/lib/context-ledger/gates.d.ts +63 -0
  21. package/dist/esm/lib/context-ledger/gates.js +105 -0
  22. package/dist/esm/lib/context-ledger/gates.js.map +1 -0
  23. package/dist/esm/lib/context-ledger/index.d.ts +10 -0
  24. package/dist/esm/lib/context-ledger/index.js +10 -0
  25. package/dist/esm/lib/context-ledger/index.js.map +1 -0
  26. package/dist/esm/lib/context-ledger/types.d.ts +107 -0
  27. package/dist/esm/lib/context-ledger/types.js +28 -0
  28. package/dist/esm/lib/context-ledger/types.js.map +1 -0
  29. package/dist/esm/lib/trace-toolpack/traceToolpack.d.ts +1 -0
  30. package/dist/esm/lib/trace-toolpack/traceToolpack.js +111 -0
  31. package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
  32. package/dist/esm/observe.d.ts +2 -0
  33. package/dist/esm/observe.js +4 -0
  34. package/dist/esm/observe.js.map +1 -1
  35. package/dist/lib/context-bisect/index.js +5 -1
  36. package/dist/lib/context-bisect/index.js.map +1 -1
  37. package/dist/lib/context-bisect/sliceToBacktrackTrace.js +154 -0
  38. package/dist/lib/context-bisect/sliceToBacktrackTrace.js.map +1 -0
  39. package/dist/lib/context-ledger/contextLedger.js +313 -0
  40. package/dist/lib/context-ledger/contextLedger.js.map +1 -0
  41. package/dist/lib/context-ledger/gates.js +111 -0
  42. package/dist/lib/context-ledger/gates.js.map +1 -0
  43. package/dist/lib/context-ledger/index.js +17 -0
  44. package/dist/lib/context-ledger/index.js.map +1 -0
  45. package/dist/lib/context-ledger/types.js +29 -0
  46. package/dist/lib/context-ledger/types.js.map +1 -0
  47. package/dist/lib/trace-toolpack/traceToolpack.js +111 -0
  48. package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
  49. package/dist/observe.js +9 -1
  50. package/dist/observe.js.map +1 -1
  51. package/dist/types/debug.d.ts +1 -1
  52. package/dist/types/debug.d.ts.map +1 -1
  53. package/dist/types/lib/context-bisect/index.d.ts +1 -0
  54. package/dist/types/lib/context-bisect/index.d.ts.map +1 -1
  55. package/dist/types/lib/context-bisect/sliceToBacktrackTrace.d.ts +53 -0
  56. package/dist/types/lib/context-bisect/sliceToBacktrackTrace.d.ts.map +1 -0
  57. package/dist/types/lib/context-ledger/contextLedger.d.ts +36 -0
  58. package/dist/types/lib/context-ledger/contextLedger.d.ts.map +1 -0
  59. package/dist/types/lib/context-ledger/gates.d.ts +64 -0
  60. package/dist/types/lib/context-ledger/gates.d.ts.map +1 -0
  61. package/dist/types/lib/context-ledger/index.d.ts +11 -0
  62. package/dist/types/lib/context-ledger/index.d.ts.map +1 -0
  63. package/dist/types/lib/context-ledger/types.d.ts +108 -0
  64. package/dist/types/lib/context-ledger/types.d.ts.map +1 -0
  65. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts +1 -0
  66. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
  67. package/dist/types/observe.d.ts +2 -0
  68. package/dist/types/observe.d.ts.map +1 -1
  69. package/package.json +4 -4
package/CLAUDE.md CHANGED
@@ -1,626 +1,87 @@
1
- # agentfootprint AI Coding Instructions
1
+ <!-- analyzed-at: 5a5b7cb @ 2026-07-02 | model: fable-5 -->
2
+ # agentfootprint — feature-work map
2
3
 
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).
4
+ Agent framework layered on footprintjs: every runner (Agent, LLMCall, compositions, patterns) is a footprintjs chart built ONCE at construction and executed on a fresh `FlowChartExecutor` per run. Engine seams (stage kinds, $-methods, engine handlers) live UPSTREAM in footprintjs and are closed here. This file maps this repo's seams and blast radius — **trust the code** where any doc disagrees.
4
5
 
5
- ## Core Thesis
6
+ ## Module map
7
+ Entry points (package.json exports): `.` core API · `/observe` ALL observability recorder factories (deliberately off the main barrel) · `/debug` + `/debug/finders` run-autopsy kit · `/events` typed event system · `/llm-providers` vendor adapters (mock, anthropic, openai, browser*) · `/memory` (defineMemory, MEMORY_TYPES, InMemoryStore, mockEmbedder) · `/memory-providers` (redis, agentcore, bedrockAgentMemory stores) · `/injection-engine` (defineSkill/Fact/Instruction/Steering, skillGraph, decideSkill) · `/tool-providers` (staticTools, gatedTools, skillScopedTools, mcpClient) · `/strategies` `/observability-providers` `/security` `/identity` `/reliability` `/resilience` `/stream` `/thinking` `/cache` `/status` `/locales`. **Main barrel does NOT export** mock/browser*/defineMemory/defineSkill/skillGraph/mcpClient/InMemoryStore — import from the subpaths above (older docs showed these on the main barrel; wrong).
6
8
 
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:
8
-
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.**
15
-
16
- ## The Mental Model — Three Slots, Six Flavors
17
-
18
- Every LLM call has three slots. Every "agent feature" is content flowing into one of them:
19
-
20
- | LLM API field | What goes here |
9
+ | src/ | one job |
21
10
  |---|---|
22
- | `system` prompt | Steering · Instruction text · Skill body · Fact data · formatted memory |
23
- | `messages` array | Conversation history · RAG chunks · memory replay · injected instructions |
24
- | `tools` array | Tool schemas (registered + Skill-attached) |
25
-
26
- The flavors are how you *mark intent* but they all reduce to one `Injection` primitive:
27
-
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 or messages |
33
- | **Fact** | Always-on (data) | system-prompt or messages |
34
-
35
- ## Mock-first development (RECOMMENDED workflow)
36
-
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.
38
-
39
- ```typescript
40
- import { Agent, mock, InMemoryStore, mockEmbedder, defineTool } from 'agentfootprint';
41
-
42
- // Mock LLM provider no API key, deterministic reply.
43
- const provider = mock({ reply: 'Refunds take 3 business days.' });
44
-
45
- // Inline-mocked tool — no real backend yet.
46
- const lookup = defineTool({
47
- schema: { name: 'lookup', description: '...', inputSchema: {} },
48
- execute: async () => 'mock data',
49
- });
50
-
51
- const agent = Agent.create({ provider, model: 'mock' }).tool(lookup).build();
52
- await agent.run({ message: 'How long does a refund take?' });
53
- ```
54
-
55
- | Boundary | Mock for development | Production swap |
56
- |---|---|---|
57
- | LLM provider | `mock({ reply })` · `mock({ replies })` for scripted ReAct | `anthropic()` · `openai()` · `bedrock()` · `ollama()` (from `agentfootprint/llm-providers`) |
58
- | Embedder | `mockEmbedder()` | OpenAI / Cohere / Bedrock embedder factory |
59
- | Memory store | `InMemoryStore` | `RedisStore` (`agentfootprint/memory-redis`) · `AgentCoreStore` (`agentfootprint/memory-agentcore`) · DynamoDB / Postgres / Pinecone (planned) |
60
- | MCP server | `mockMcpClient({ tools })` in-memory, no SDK | `mcpClient({ transport })` to a real server |
61
- | Tool execute | inline `async () => '...'` closure | real implementation |
62
-
63
- 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.
64
-
65
- **Subpath imports** for memory adapters keep the main barrel small + tree-shaking clean:
66
-
67
- ```typescript
68
- import { RedisStore } from 'agentfootprint/memory-redis';
69
- import { AgentCoreStore } from 'agentfootprint/memory-agentcore';
70
- ```
71
-
72
- Both lazy-require their SDK (`ioredis` / `@aws-sdk/client-bedrock-agent-runtime`) and accept `_client` for test injection.
73
-
74
- **Multi-turn mock for tool-using ReAct:**
75
-
76
- ```typescript
77
- const provider = mock({
78
- replies: [
79
- { toolCalls: [{ id: '1', name: 'lookup', args: { topic: 'refunds' } }] },
80
- { content: 'Refunds take 3 business days.' },
81
- ],
82
- });
83
- ```
84
-
85
- Each `complete()` consumes one reply in order. Exhaustion throws loud misnumbered scripts fail tests instead of silently looping.
86
-
87
- ## Public API
88
-
89
- ### MCP — `mcpClient` (connect to MCP servers, register their tools)
90
-
91
- ```typescript
92
- import { Agent, mcpClient } from 'agentfootprint';
93
-
94
- const slack = await mcpClient({
95
- name: 'slack',
96
- transport: { transport: 'stdio', command: 'npx', args: ['@example/slack-mcp'] },
97
- });
98
-
99
- const agent = Agent.create({ provider })
100
- .tools(await slack.tools()) // pull ALL tools from the server in one call
101
- .build();
102
-
103
- await agent.run({ message: '...' });
104
- await slack.close();
105
- ```
106
-
107
- Transports: `stdio` (local subprocess), `http` (Streamable HTTP). The
108
- `@modelcontextprotocol/sdk` peer-dep is lazy-required — zero runtime
109
- cost when MCP isn't used. Friendly install hint if missing.
110
-
111
- `agent.tools(arr)` is the bulk-register companion to `agent.tool(t)`.
112
- Pair with `await client.tools()` to register everything an MCP server
113
- exposes in one builder call. Tool-name uniqueness is still validated
114
- at `.build()` across MCP servers + manual `.tool()` calls.
115
-
116
- ### RAG — `defineRAG` (one factory, one helper)
117
-
118
- ```typescript
119
- import {
120
- defineRAG, indexDocuments,
121
- InMemoryStore, mockEmbedder,
122
- } from 'agentfootprint';
123
-
124
- const embedder = mockEmbedder();
125
- const store = new InMemoryStore();
126
-
127
- // Seed the corpus once at startup
128
- await indexDocuments(store, embedder, [
129
- { id: 'doc1', content: 'Refunds are processed within 3 business days.' },
130
- { id: 'doc2', content: 'Pro plan costs $20/month.' },
131
- ]);
132
-
133
- // Define the retriever
134
- const docs = defineRAG({
135
- id: 'product-docs',
136
- store, embedder,
137
- topK: 3,
138
- threshold: 0.7, // STRICT — no fallback when nothing matches
139
- asRole: 'user', // chunks land as user-role context (RAG default)
140
- });
141
-
142
- // Wire to agent — `.rag()` is an alias for `.memory()`, same plumbing
143
- agent.rag(docs);
144
- ```
145
-
146
- `defineRAG` is sugar over `defineMemory({ type: SEMANTIC, strategy: TOP_K })`. Same plumbing, different intent: RAG = document corpus retrieval; `defineMemory` = conversation/run-state memory.
147
-
148
- ### Agent (ReAct primitive)
149
-
150
- ```typescript
151
- import { Agent, defineTool } from 'agentfootprint';
152
- import { anthropic } from 'agentfootprint/llm-providers';
153
-
154
- const agent = Agent.create({
155
- provider: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }),
156
- model: 'claude-sonnet-4-5-20250929',
157
- maxIterations: 10,
158
- })
159
- .system('You are a helpful assistant.')
160
- .tool(weatherTool)
161
- .build();
162
-
163
- const result = await agent.run({ message: 'Weather in SF?' });
164
- ```
165
-
166
- Builder methods:
167
- - `.system(prompt)` — base system prompt
168
- - `.tool(definedTool)` — register a tool
169
- - `.steering(injection)` · `.instruction(injection)` · `.skill(injection)` · `.fact(injection)` — context-engineering injections
170
- - `.memory(definition)` — register a memory (returned by `defineMemory()`)
171
- - `.build()` → `Agent` — runner with `.run({ message, identity? })`
172
-
173
- ### LLMCall (one-shot primitive)
174
-
175
- ```typescript
176
- import { LLMCall } from 'agentfootprint';
177
- import { anthropic } from 'agentfootprint/llm-providers';
178
-
179
- const call = LLMCall.create({ provider: anthropic(...), model: 'claude-sonnet-4-5-20250929' })
180
- .system('You are a terse assistant.')
181
- .build();
182
-
183
- const answer = await call.run({ message: 'Summarize: ...' });
184
- ```
185
-
186
- ### Tools
187
-
188
- ```typescript
189
- import { defineTool } from 'agentfootprint';
190
-
191
- const weather = defineTool({
192
- schema: {
193
- name: 'weather',
194
- description: 'Current weather for a city.',
195
- inputSchema: {
196
- type: 'object',
197
- properties: { city: { type: 'string' } },
198
- required: ['city'],
199
- },
200
- },
201
- execute: async (args) => `${(args as { city: string }).city}: 72°F`,
202
- });
203
- ```
204
-
205
- Tool-name uniqueness is validated at `agent.build()` time across `.tool()` registrations AND every Skill's `inject.tools[]`.
206
-
207
- ### Context Engineering — 4 typed factories
208
-
209
- All four return an `Injection` evaluated by the same engine; all emit the same `agentfootprint.context.injected` event with `source` discriminating the flavor.
210
-
211
- ```typescript
212
- import {
213
- defineSkill, defineSteering, defineInstruction, defineFact,
214
- } from 'agentfootprint';
215
-
216
- // Always-on rule (system-prompt)
217
- const tone = defineSteering({
218
- id: 'tone',
219
- prompt: 'Be friendly and concise.',
220
- });
221
-
222
- // Predicate-gated
223
- const urgent = defineInstruction({
224
- id: 'urgent',
225
- activeWhen: (ctx) => /urgent|asap/i.test(ctx.userMessage),
226
- prompt: 'Prioritize the fastest path to resolution.',
227
- });
228
-
229
- // Dynamic ReAct — fires AFTER a specific tool returned (recency-weighted slot)
230
- const afterRedact = defineInstruction({
231
- id: 'after-redact',
232
- activeWhen: (ctx) => ctx.lastToolResult?.toolName === 'redact_pii',
233
- prompt: 'Use the redacted text only. Do not paraphrase the original.',
234
- slot: 'messages', // higher LLM attention than system-prompt
235
- });
236
-
237
- // LLM-activated body + tools (auto-attaches `read_skill` activation tool)
238
- const billing = defineSkill({
239
- id: 'billing',
240
- description: 'Use for refunds, subscriptions, invoices.',
241
- body: 'Confirm identity before processing refunds.',
242
- tools: [refundTool],
243
- });
244
-
245
- // Developer-supplied data (not behavior)
246
- const userProfile = defineFact({
247
- id: 'user',
248
- data: 'User: Alice (alice@example.com), Plan: Pro.',
249
- });
250
-
251
- agent
252
- .steering(tone)
253
- .instruction(urgent)
254
- .instruction(afterRedact)
255
- .skill(billing)
256
- .fact(userProfile);
257
- ```
258
-
259
- ### Skill graph — declarative, drawable skill routing (`skillGraph`)
260
-
261
- `skillGraph()` is NOT a flavor — it's a builder that sets the **trigger** on a set
262
- of **skills** (and draws the routing). It throws on a non-skill. Sugar over the
263
- trigger model; zero engine change.
264
-
265
- ```typescript
266
- import { skillGraph, decide, type CombinedRecorder } from 'agentfootprint';
267
-
268
- // Flat: entry skills + routing edges
269
- const graph = skillGraph()
270
- .entry(triage) // trigger: always / rule
271
- .route(triage, sfp, { when: (r) => JSON.parse(r.result).crc > 0, label: 'CRC>0' })
272
- .route(triage, lookup, { onToolReturn: 'get_fcns' }) // trigger: on-tool-return
273
- .build();
274
-
275
- // Decision tree: predicate nodes route to skill leaves (exactly one fires)
276
- const tree = skillGraph()
277
- .tree(decide(isIo, ioSkill, decide(isSfp, sfpSkill, triage, 'sfp?'), 'io intent?'))
278
- .build();
279
-
280
- agent.skillGraph(graph); // mount (sugar over .injection per skill)
281
- graph.toMermaid(); // draws itself (predicate diamonds → skill boxes)
282
- graph.nodes; graph.edges; // the drawn shape (for a custom renderer)
283
- ```
284
-
285
- - `.entry(skill, { when? })` → `always` / `rule`; `.route(a, b, { when | onToolReturn })`
286
- → `rule` / `on-tool-return`; a bare `.route` keeps the skill's default `llm-activated`
287
- (model-reachable via `read_skill`, drawn dashed). `.tree(decide(...))` compiles each
288
- leaf to the conjunction of its root→leaf predicates (with sibling negation → exactly
289
- one leaf).
290
- - **On-demand tools (the headline win):** because a `.tree()` routes to exactly ONE leaf
291
- per turn, each leaf is stamped `autoActivate: 'currentSkill'` **by default** — so a
292
- skill's `inject.tools` reach the LLM ONLY when the tree routes there, instead of every
293
- skill's tools landing in the always-on static registry on every call. (`read_skill`
294
- stays as the escape hatch.) Opt out with `.tree(root, { scopeTools: false })`; a leaf
295
- that sets its own `autoActivate` in `defineSkill(...)` is always respected. Flat
296
- `.entry()`/`.route()` graphs are **not** auto-scoped (multiple may be active) — set
297
- `autoActivate: 'currentSkill'` on those skills yourself if you want per-skill gating.
298
- - **Routing provenance:** each compiled skill carries `metadata.skillGraph` (a
299
- `SkillRouting` — `via` + decision `path`). At run time it rides out on
300
- `context.evaluated.routing` (per active skill: id, path, edge label, unlocked tools),
301
- narrated by the `context.routed` commentary line. The lens `<SkillGraphFlow>` shows the
302
- same path ("REACHED WHEN").
303
- - Lens viewer: `import { SkillGraphFlow } from 'agentfootprint-lens'` — interactive
304
- two-panel (graph │ click-to-inspect skill detail + decision path).
305
-
306
- ### Memory — `defineMemory({ type, strategy, store })`
307
-
308
- ONE factory dispatches `type × strategy.kind` onto the right pipeline. Multiple memories layer cleanly via per-id scope keys (`memoryInjection_${id}`).
309
-
310
- ```typescript
311
- import {
312
- defineMemory,
313
- MEMORY_TYPES, MEMORY_STRATEGIES, SNAPSHOT_PROJECTIONS,
314
- InMemoryStore, mockEmbedder,
315
- } from 'agentfootprint';
316
-
317
- // Short-term sliding window — the 90% case
318
- const shortTerm = defineMemory({
319
- id: 'short-term',
320
- type: MEMORY_TYPES.EPISODIC,
321
- strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
322
- store: new InMemoryStore(),
323
- });
324
-
325
- // Semantic recall — vector retrieval with strict threshold
326
- const facts = defineMemory({
327
- id: 'facts',
328
- type: MEMORY_TYPES.SEMANTIC,
329
- strategy: {
330
- kind: MEMORY_STRATEGIES.TOP_K,
331
- topK: 3,
332
- threshold: 0.7, // STRICT — empty when no match
333
- embedder: mockEmbedder(), // swap for openaiEmbedder() in prod
334
- },
335
- store: new InMemoryStore(),
336
- });
337
-
338
- // Causal — UNIQUE TO AGENTFOOTPRINT. Persists run snapshots so cross-run
339
- // "why was X rejected?" follow-ups answer from the STORED run: decisions
340
- // (footprintjs decide()/select() evidence + route/skill-graph provenance),
341
- // tool calls (args + result previews), iterations, duration, token usage —
342
- // harvested automatically by causalEvidenceRecorder when a CAUSAL memory is
343
- // mounted. (commitLog/narrative capture: not yet — see SnapshotEntry.)
344
- const causal = defineMemory({
345
- id: 'causal',
346
- type: MEMORY_TYPES.CAUSAL,
347
- strategy: {
348
- kind: MEMORY_STRATEGIES.TOP_K,
349
- topK: 1,
350
- threshold: 0.7,
351
- embedder: mockEmbedder(),
352
- },
353
- store: new InMemoryStore(),
354
- projection: SNAPSHOT_PROJECTIONS.DECISIONS,
355
- });
356
-
357
- agent.memory(shortTerm).memory(facts).memory(causal);
358
-
359
- // Multi-tenant identity is plumbed through agent.run:
360
- await agent.run({
361
- message: '...',
362
- identity: { tenant: 'acme', principal: 'alice', conversationId: 'thread-42' },
363
- });
364
- ```
365
-
366
- The 4 memory **types**:
367
- - `EPISODIC` — raw conversation messages
368
- - `SEMANTIC` — extracted structured facts
369
- - `NARRATIVE` — beats / summaries of prior runs
370
- - `CAUSAL` — footprintjs decision-evidence snapshots ⭐
371
-
372
- The 7 **strategies**:
373
- - `WINDOW` (rule, last N) · `BUDGET` (decider, fit-to-tokens) · `SUMMARIZE` (LLM compresses older)
374
- - `TOP_K` (score-threshold) · `EXTRACT` (LLM distills on write)
375
- - `DECAY` (recency-weighted, planned) · `HYBRID` (compose multiple)
376
-
377
- ### Compositions — Multi-Agent via Control Flow
378
-
379
- There is **no** `MultiAgentSystem` class. Multi-agent = compositions of single Agents through the same control flow that connects any flowchart stages:
380
-
381
- ```typescript
382
- import { Sequence, Parallel, Conditional, Loop } from 'agentfootprint';
383
-
384
- // Output flows downstream — each step takes an id + a runner (Agent / LLMCall / runner)
385
- const pipeline = Sequence.create()
386
- .step('research', researcher)
387
- .step('write', writer)
388
- .step('edit', editor)
389
- .build();
390
-
391
- // Multi-perspective with LLM merge — each branch takes an id + a runner
392
- const tot = Parallel.create()
393
- .branch('t1', thoughtAgent)
394
- .branch('t2', thoughtAgent)
395
- .branch('t3', thoughtAgent)
396
- .mergeWithLLM({ provider, model: 'claude-sonnet-4-5-20250929', prompt: 'Rank and synthesize.' })
397
- .build();
398
-
399
- // Predicate-based routing — .when(id, predicate, runner); exactly one .otherwise(id, runner).
400
- // predicate receives ConditionalInput { message }.
401
- const triage = Conditional.create()
402
- .when('billing', (input) => /refund|invoice/i.test(input.message), billingAgent)
403
- .when('tech', (input) => /error|bug/i.test(input.message), techAgent)
404
- .otherwise('general', generalAgent)
405
- .build();
406
-
407
- // Iterate with budget — .repeat(runner) sets the body, .times(n) caps iterations.
408
- // until() guard receives { iteration, latestOutput, startMs }.
409
- const refine = Loop.create()
410
- .repeat(critiqueAgent)
411
- .until((ctx) => /done/i.test(ctx.latestOutput))
412
- .times(5)
413
- .build();
414
- ```
415
-
416
- ### Named patterns — recipes ship as runnable examples
417
-
418
- ```
419
- ReAct = Agent (default loop)
420
- Reflexion = Sequence(Agent, critique-LLM, Agent)
421
- Tree-of-Thoughts = Parallel(Agent × N) + rank
422
- Self-Consistency = Parallel(Agent × N) + majority-vote
423
- Debate = Loop(Agent × 2 + judge)
424
- Map-Reduce = Parallel(Agent × N) + merge
425
- Swarm = Agent whose tools are other Agents
426
- ```
427
-
428
- Browse [`examples/patterns/`](examples/patterns/) — every pattern is a runnable end-to-end test.
429
-
430
- ### Providers
431
-
432
- ```typescript
433
- import { mock } from 'agentfootprint'; // zero-peer-dep, on the main barrel
434
- import { anthropic, openai, azureOpenai, bedrock, ollama } from 'agentfootprint/llm-providers'; // vendor-SDK-backed
435
-
436
- // Adapter-swap testing: same agent, different provider, $0 in CI
437
- const provider = process.env.NODE_ENV === 'production'
438
- ? anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! })
439
- : mock({ reply: 'test response' });
440
-
441
- // Company endpoints (3 buckets):
442
- // 1. OpenAI-compatible (Together/Groq/OpenRouter/vLLM/LiteLLM): openai({ baseURL, apiKey })
443
- // 2. Azure OpenAI (*.openai.azure.com — deployment-as-model, api-key, api-version):
444
- const azure = azureOpenai({
445
- endpoint: process.env.OPENAI_BASE_URL, // *.openai.azure.com
446
- apiKey: process.env.AZURE_OPENAI_API_KEY,
447
- apiVersion: process.env.AZURE_OPENAI_API_VERSION, // 2024-12-01-preview
448
- deployment: process.env.MODEL_NAME, // gpt-4o-128k (the deployment)
449
- }); // Agent.create({ provider: azure, model: 'azure' })
450
- // 3. Anything else: implement the LLMProvider interface (~30 lines).
451
-
452
- // Env-driven (no branching): reads .env, detects the provider, returns it.
453
- import { providerFromEnv } from 'agentfootprint';
454
- const { provider: p, model } = providerFromEnv({ fallbackToMock: true });
455
- // Azure (AZURE_OPENAI_API_KEY + endpoint) → anthropic (ANTHROPIC_API_KEY) → openai (OPENAI_API_KEY) → mock
456
- ```
457
-
458
- Every provider implements the same `LLMProvider` interface. The vendor-SDK providers (`anthropic`, `openai`, `azureOpenai`, `bedrock`, `ollama`, plus their `*Provider` classes) live ONLY at `agentfootprint/llm-providers` (legacy alias `agentfootprint/providers`) — kept off the main barrel so bundlers never walk the lazy peer-dep requires. The main barrel exports the zero-peer-dep providers: `mock`, `browserAnthropic`, `browserOpenai`, `browserAzureOpenai`, plus the resolvers `createProvider` (by `kind`) and `providerFromEnv` (env auto-detect, Node-only). **Azure is NOT OpenAI-compatible** — use `azureOpenai()` (Node) / `browserAzureOpenai()` (browser, `api-key` header, deployment-scoped URL), not `openai({ baseURL })`. Full list + the "connect a company endpoint" buckets + `providerFromEnv()` table: [docs/guides/adapters.md](docs/guides/adapters.md).
459
-
460
- ### Pause / Resume (Human-in-the-Loop)
461
-
462
- ```typescript
463
- import { askHuman, pauseHere, isPaused } from 'agentfootprint';
464
-
465
- const approveTool = defineTool({
466
- schema: { name: 'approve', description: 'Ask a human.', inputSchema: { ... } },
467
- // askHuman()/pauseHere() THROW a PauseRequest — call them INSIDE execute,
468
- // do not assign them as the execute function.
469
- execute: async (args) => {
470
- askHuman({ question: `Approve this action?`, risk: 'high' });
471
- return ''; // unreachable — askHuman always throws
472
- },
473
- });
474
-
475
- const result = await agent.run({ message: 'Refund $500?' });
476
- if (isPaused(result)) {
477
- const checkpoint = result.checkpoint; // JSON-serializable
478
- // Persist to Redis/DB; later, on possibly different server:
479
- const final = await agent.resume(checkpoint, { approved: true });
480
- }
481
- ```
482
-
483
- ### Resilience
484
-
485
- ```typescript
486
- import { withRetry, withFallback, fallbackProvider } from 'agentfootprint/resilience';
487
- import { anthropic, openai, ollama } from 'agentfootprint/llm-providers';
488
-
489
- const reliable = withRetry(provider, { maxAttempts: 3 });
490
- const resilient = withFallback(primary, fallback);
491
- const chain = fallbackProvider(anthropic({...}), openai({...}), ollama({...}));
492
- ```
493
-
494
- Also available: `withCircuitBreaker(provider, opts?)`. The resilience decorators live ONLY at `agentfootprint/resilience` (not the main barrel).
495
-
496
- ### Observability — 65 typed events × 18 domains
497
-
498
- ```typescript
499
- agent.on('agentfootprint.context.injected', (e) =>
500
- console.log(`[${e.payload.source}] landed in ${e.payload.slot}`));
501
- agent.on('agentfootprint.stream.tool_start', (e) =>
502
- console.log(`→ ${e.payload.toolName}(${JSON.stringify(e.payload.args)})`));
503
- agent.on('agentfootprint.agent.turn_end', (e) =>
504
- console.log(`[${e.payload.iterationCount} iter, ${e.payload.totalInputTokens}+${e.payload.totalOutputTokens} tokens]`));
505
- ```
506
-
507
- 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`.
508
-
509
- **Listener lifecycle (long-lived runners / servers):** subscriptions and attached recorders live for the RUNNER's lifetime — NOTHING auto-expires per-run; the caller owns cleanup. Three release paths for listeners:
510
-
511
- ```typescript
512
- const unsub = agent.on('agentfootprint.agent.turn_end', fn); // 1. keep + call the Unsubscribe
513
- agent.on('*', fn, { signal: requestController.signal }); // 2. AbortSignal — auto-unsubscribes on abort (also on .once())
514
- agent.removeAllListeners(); // 3. bulk escape hatch (listeners ONLY — attach()ed recorders untouched)
515
- ```
516
-
517
- `agent.listenerCount()` is the leak diagnostic — no-arg = total retained; `listenerCount(key)` = that exact subscription bucket (wildcards not folded in; "would anything fire" is the dispatcher's `hasListenersFor`). Bounded-leak guarantee: every removal path (unsubscribe, `off()`, signal abort, once-fire, `removeAllListeners()`) prunes emptied buckets AND detaches the abort handler from the consumer's signal, so dispatcher storage is bounded by LIVE subscriptions — never subscription history. Per-run pattern on a reused server agent: subscribe with a per-request `AbortSignal`, abort after the run. Recorders via `attach()` follow rule 1 only — keep the returned Unsubscribe (`removeAllListeners()` does not detach them; `enable.*` strategy listeners DO get dropped by it, so re-enable afterwards if needed).
518
-
519
- **"What the model saw" (debugging tool/skill selection):**
520
- - `stream.llm_start.tools` — the tool CATALOG (`{ name, description }[]`) the model saw
521
- for the call (its menu); `toolsCount` reflects the dynamic set actually sent.
522
- - `context.evaluated.skillCatalog` — the skill menu offered; `.routing` — which
523
- skill-graph injection activated + why (decision path / edge + unlocked tools).
524
- - `agentThinkingTrace` surfaces both: `toolsSeen` on the ask/answer beat (AgentThinkingUI
525
- ≥0.10 renders "Tools the model saw (N)") + the `context.routed` lead-in.
526
-
527
- Recorders (auto-attached when relevant builder method is called):
528
- - `ContextRecorder` — `context.evaluated` / `context.injected` / `context.slot_composed`
529
- - `streamRecorder` — `stream.llm_start` / `stream.llm_end` / `stream.token` / `stream.tool_start` / `stream.tool_end`
530
- - `agentRecorder` — `agent.turn_start` / `agent.turn_end` / `agent.iteration_start` / `agent.iteration_end` / `agent.route_decided`
531
- - `costRecorder` — `cost.tick` / `cost.limit_hit` (when `pricingTable` supplied)
532
- - `permissionRecorder` — `permission.check` (when `permissionChecker` supplied)
533
- - `evalRecorder` · `memoryRecorder` · `skillRecorder`
534
-
535
- **Observer delivery tier (RFC-001 Block 10):** `Agent.create({ observerDelivery:
536
- 'deferred' })` routes the bridge recorders above + consumer `.recorder()` /
537
- `agent.attach()` recorders through footprintjs's bounded capture queue —
538
- capture inline (≈ µs), deliver one beat behind, drain synchronously at run
539
- resolve / reject / pause. Default `'inline'` = byte-identical attach path, no
540
- queue allocated. `agent.on()` listeners receive deep-equal typed events either
541
- way (parity-tested). EXCEPTION kept inline: the causal-evidence recorder — the
542
- memory write stage reads `collect()` MID-run. A recorder's own `delivery`
543
- field beats the agent default (per-recorder override). Dials via
544
- `observerDeliveryOptions` (throws without `'deferred'`); shutdown via
545
- `agent.drainObservers({ timeoutMs })`; stats on
546
- `getLastSnapshot()?.observerStats`. CONTRACT: typed event payloads must be
547
- detached plain data — never pass a TypedScope read (e.g. `scope.history`, a
548
- live deep-Proxy) into `typedEmit`; use the plain local value (`typedEmit`
549
- dev-warns on unclonable payloads). Bench:
550
- `examples/features/21-deferred-observers.ts`.
551
-
552
- ### Self-explain — `.selfExplain()` (the agent answers "why?" about its own last run)
553
-
554
- 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`.
555
-
556
- ## Anti-Patterns — Don't
557
-
558
- - ❌ **Don't ship a `ReflexionAgent` class.** Compose `Sequence(Agent, critique-LLM, Agent)`.
559
- - ❌ **Don't use `agent.run('string')`** — use `agent.run({ message: '...', identity? })`.
560
- - ❌ **Don't import from removed subpaths** like `'agentfootprint/instructions'` — the injection factories are on the main barrel (or `'agentfootprint/injection-engine'`). NOTE: `'agentfootprint/observe'`, `'agentfootprint/security'`, `'agentfootprint/resilience'`, `'agentfootprint/llm-providers'` ARE real, current subpaths — some symbols (e.g. `buildStepGraph`, `extractSequence`, the resilience decorators, the vendor-SDK providers) are intentionally subpath-only, NOT on the main barrel.
561
- - ❌ **Don't use `.memoryPipeline(pipeline)`** — that's the v1 API. Use `.memory(defineMemory({...}))`.
562
- - ❌ **Don't fall back when TopK threshold returns nothing.** Strict semantics: garbage past context > none is wrong.
563
- - ❌ **Don't store closures or class instances in scope** — TransactionBuffer can't clone functions. Memory-store entries serialize to JSON.
564
- - ❌ **Don't add new event types per feature.** Route through `agentfootprint.context.injected` with a new `source` value.
565
- - ❌ **Don't reach into `getArgs()` / `getEnv()` from injection content.** Predicates run with the engine's `InjectionContext` only.
566
-
567
- ## Decision Tree — Pick the Right Tool
568
-
569
- | Goal | Use |
570
- |---|---|
571
- | One-shot LLM call (summarization, classification) | `LLMCall` |
572
- | Loop with tools (research, code, anything iterative) | `Agent` |
573
- | Two LLM calls in series with output flowing | `Sequence` |
574
- | Multiple critics, merge with LLM | `Parallel` |
575
- | Route to specialist by intent | `Conditional` |
576
- | Iterate until quality bar | `Loop` |
577
- | Output format / persona / safety policy | `defineSteering` |
578
- | Rule that fires when predicate matches | `defineInstruction` |
579
- | LLM activates a body of expertise + its tools | `defineSkill` |
580
- | Inject user profile / current time / env data | `defineFact` |
581
- | Remember last N turns of conversation | `defineMemory({ type: EPISODIC, strategy: WINDOW })` |
582
- | Semantic recall via embeddings | `defineMemory({ type: SEMANTIC, strategy: TOP_K })` |
583
- | Cross-run "why?" replay (decisions + tool evidence auto-harvested) | `defineMemory({ type: CAUSAL, strategy: TOP_K })` ⭐ |
584
- | Long conversation overflows context | `defineMemory({ type: EPISODIC, strategy: SUMMARIZE })` |
585
- | Retrieve from a document corpus | `defineRAG({ store, embedder, topK, threshold })` |
586
- | Use tools from an external MCP server | `mcpClient({ transport, ... })` + `agent.tools(await c.tools())` |
587
-
588
- ## Build & Test
589
-
590
- ```bash
591
- npm install agentfootprint footprintjs
592
- npm test # vitest run — 2000+ tests
593
- npm run example examples/... # run a single example end-to-end
594
- npm run test:examples # typecheck + run every example end-to-end
595
- ```
596
-
597
- ## Package layout
598
-
599
- ```
600
- src/
601
- ├── core/ — Agent, LLMCall, builder methods, pause/resume
602
- ├── core-flow/ — Sequence, Parallel, Conditional, Loop
603
- ├── patterns/ — Reflexion, SelfConsistency, ToT, Debate, MapReduce, Swarm
604
- ├── lib/
605
- │ └── injection-engine/ — Injection primitive + 4 factories + engine subflow
606
- ├── memory/ — defineMemory + 4 types × 7 strategies + InMemoryStore + Causal
607
- ├── adapters/llm/ — Anthropic, OpenAI, Bedrock, Ollama, Browser variants, Mock
608
- ├── recorders/ — context, stream, agent, cost, skill, permission, eval, memory
609
- ├── resilience/ — withRetry, withFallback, fallbackProvider, withCircuitBreaker
610
- └── stream.ts — SSE formatter
611
-
612
- examples/ — 47 runnable end-to-end tests organized by DNA layer
613
- ├── core/ — primitives
614
- ├── core-flow/ — compositions
615
- ├── patterns/ — canonical recipes
616
- ├── context-engineering/ — InjectionEngine flavors
617
- ├── memory/ — 7 strategies
618
- └── features/ — pause/cost/permissions/observability/events
619
- ```
620
-
621
- ## Roadmap (informs what to defer)
622
-
623
- - **Shipped (v3.x current)** — primitives + compositions + InjectionEngine + Memory (incl. Causal) + providers (vendor-SDK + browser + mock) + RAG (`defineRAG`) + MCP (`mcpClient`) + Redis/AgentCore memory adapters + resilience (`withRetry` / `withFallback` / `fallbackProvider` / `withCircuitBreaker`) + reliability subsystem + governance (`PermissionPolicy`) + observability subpaths (`/observe`, `/thinking`, `/locales`, `/status`)
624
- - **Planned** — BudgetTracker · DynamoDB / Postgres / Pinecone memory adapters · Causal training-data exports (SFT / DPO / process-RL) · Deep Agents · A2A protocol · Lens UI integration
625
-
626
- When in doubt — read [`examples/`](examples/), every file is a runnable spec.
11
+ | core/ | primitives: Agent.ts (ReAct runner), LLMCall.ts, RunnerBase.ts (dispatcher + attach + enable.*), tools.ts (defineTool), pause.ts, runCheckpoint.ts |
12
+ | core/agent/ | chart assembly: buildAgentChart / buildDynamicAgentChart (picked by reactMode, Agent.ts:1145-1147), buildToolRegistry, stages/ (seed, callLLM, route, toolCalls, prepareFinal, breakFinal, reliabilityExecution) |
13
+ | core/slots/ | the 3 context-slot subflow builders + thinking subflow — intentionally NOT exported |
14
+ | core-flow/ | Sequence/Parallel/Conditional/Loop — RunnerBase subclasses with own charts |
15
+ | patterns/ | Debate/MapReduce/Reflection/SelfConsistency/Swarm/ToT pure composition of runners, no new control flow |
16
+ | adapters/ | hexagonal ports (types.ts = ALL port interfaces) + vendor impls (llm/, memory/, identity/, observability/) |
17
+ | recorders/core/ | bridges footprintjs events → typed EventDispatcher (ContextRecorder, EmitBridge, typedEmit) — auto-attached by Agent.createExecutor; most factories also exported via `/observe` for manual wiring (EmitBridge itself stays internal) |
18
+ | recorders/observability/ | consumer recorders over the typed stream (RunStepRecorder, FlowchartRecorder, Status, Trace replay) |
19
+ | lib/ | first-party sub-libraries: injection-engine/, context-bisect/ (localizeContextBug, toBacktrackTrace + sliceToBacktrackTrace — the atui board serializers), influence-core/, trace-toolpack/ (selfExplain; 6 tools incl. variable-first `backtrack(variable, element?)`), context-ledger/ (which pieces EARNED their tokens — post-run offers/uses/outcomes bookkeeping + demote-never-starve gates `ledgerToolGate`/`ledgerEntryScorer`/`ledgerGated`; grouped-mode folds sf-llm-call inner logs, unmeterable runs → undefined; /observe), mcp/, rag/, tool-lint/ |
20
+ | memory/ | store/ (MemoryStore port) + pipeline presets + stages + beats/facts + causal/ (dev-only, TOP_K+search()-only) + wire/mountMemoryPipeline |
21
+ | events/ | EventDispatcher (wildcard subs), registry (EVENT_NAMES, AgentfootprintEventMap), payloads |
22
+ | conventions.ts | THE builder↔recorder protocol: SUBFLOW_IDS/STAGE_IDS (internal), INJECTION_KEYS/stageRole/milestoneFor (exported, Lens-facing) |
23
+
24
+ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `debug/finders.ts` re-exports them — only the old subpath is deprecated), while real recorders live in `recorders/observability/`; `identity` appears twice (src/identity.ts = tool credentials; memory/identity/ = tenant scoping — unrelated); `resilience` (provider decorators) ≠ `reliability` (in-loop rules gate).
25
+
26
+ ## Core state & flow
27
+ - `AgentState` (core/agent/types.ts:287) — THE chart state; every stage gets `TypedScope<AgentState>`. Mutability conventions documented ON the type (:276-286). Everything in scope must survive structuredClone → functions stay in closures, errors stringified, injections projected to POJOs.
28
+ - **Two runId namespaces**: typed-event `meta.runId` (Agent's makeRunId, RunnerBase.ts:55) vs footprintjs `traversalContext.runId` — never correlate across them.
29
+ - **Event path**: stage `typedEmit` → `scope.$emit` → footprintjs emit channel → `EmitBridge.onEmit` (drops if no dispatcher listener! EmitBridge.ts:44) → `buildEventMeta` → dispatcher → `agent.on()` listeners. Fires MID-STAGE, **before that stage's commit** — correlate events↔commits by runtimeStageId, never arrival order.
30
+ - Executor defaults DIVERGE from footprintjs: `readTracking:'summary'`, `commitValues:'delta'` (Agent.ts:779-782) — read commit values via `commitValueAt`, never `bundle.overwrite[key]`.
31
+ - Recorders never read AgentState directly; slot subflows write `INJECTION_KEYS` convention keys, `ContextRecorder.onWrite` resolves the slot from the write's own runtimeStageId (parallel-safe).
32
+ - $break is the only clean-stop channel; structured fail context rides scope fields (`policyHalt*`, `reliabilityFail*`), decoded post-run by `Agent.finalizeResult` (Agent.ts:884) into typed errors.
33
+
34
+ ## Extension points
35
+ - **Tool**: `defineTool` (core/tools.ts:155); shape `Tool = {schema, needs?, execute(args, ctx)}` (:23). Register `AgentBuilder.tool()` (AgentBuilder.ts:184); merged with auto `read_skill` in buildToolRegistry (:65; same-reference skill tools dedupe, any other name collision THROWS at build). Chart-as-tool: `flowchartAsTool` (core/flowchartAsTool.ts:203). MCP: `mcpClient(...).tools()`.
36
+ - **ToolProvider** (per-iteration visibility): `list(ctx): Tool[]` (tool-providers/types.ts:121); max ONE per agent (AgentBuilder.ts:238 throws on second); combinators staticTools/gatedTools/skillScopedTools chain decorator-style.
37
+ - **LLM provider**: `LLMProvider = {name, complete, stream?}` (adapters/types.ts:230) passed as `AgentOptions.provider`. `provider.name` keys THREE auto-resolutions: cache strategy (cache/strategyRegistry.ts:40), thinking handler (thinking/registry.ts:43), Lens labels.
38
+ - **Recorder, 3 layers**: (1) raw footprintjs CombinedRecorder via `agent.attach()` (RunnerBase.ts:474 — NOT idempotent); (2) typed stream `agent.on(type|'*')`; (3) new built-in = factory taking `{dispatcher, getRunContext}`, registered in the attach block inside `Agent.run()` (Agent.ts:807-845), barreled in src/observe.ts.
39
+ - **New typed event (3-step)**: payload interface in events/payloads.ts + entry in `AgentfootprintEventMap` (registry.ts:198) + append to `ALL_EVENT_TYPES` (registry.ts:488, count-asserted by tests). New DOMAIN also needs a bridge attach in Agent.createExecutor or emits never reach the dispatcher — AND a hand-edit to `DomainWildcard` (dispatcher.ts:67-82; already missing validation/credential/reliability).
40
+ - **Strategy (vendor sink)**: shapes in strategies/types.ts (Observability :130, Cost :169, LiveStatus :201, Lens :234); attach via `agent.enable.*` or `registerObservabilityStrategy` (strategies/registry.ts). New vendor = export from observability-providers.ts, NOT a new subpath.
41
+ - **Memory store**: implement `MemoryStore` (memory/store/types.ts:113; `search?` REQUIRED for causal memory); pass to `defineMemory({store})`. Memory TYPE/STRATEGY unions are CLOSED (define.types.ts:57/74 new one edits defineMemory dispatch + a pipeline builder).
42
+ - **Injection/skill**: `Injection = {id, flavor, trigger, inject}` (lib/injection-engine/types.ts:161); trigger is a closed 4-variant union (:30 — new kind edits evaluator.ts:40-74 switch). Factories defineSkill etc.; skill graph via `skillGraph()` (skillGraph.ts:347) with pluggable `EntryScorer` (entryScorer.ts:64).
43
+ - **Ports table** (wired via AgentOptions): PermissionChecker (adapters/types.ts:403), PricingTable (:412), CredentialProvider (identity/types.ts:89), CacheStrategy (cache/types.ts:151, registerCacheStrategy), ThinkingHandler (thinking/types.ts:114 — auto-wire scans HARDCODED SHIPPED_THINKING_HANDLERS, registry.ts:26), ReliabilityConfig (reliability/types.ts:183), OutputSchemaParser (core/outputSchema.ts:62, duck-typed).
44
+ - **Finder** (context-error localization): conform to `Finder` (observability/contextError/finders/types.ts:92); NO registry by design — one file + barrel line. Pluggable `InfluenceScorer` via `localizeContextBug({scorer})` (lib/context-bisect/localize.ts:340).
45
+ - **New composition/pattern**: extend RunnerBase, expose `getSpec(): FlowChart`, compose others' specs — no engine change.
46
+ - **Closed seams**: Agent chart internals (AgentChartDeps not exported extend via injections/tools/memory/thinking, never by adding a ReAct stage); ContextSlot (3 slots fixed); ProviderKind factory; dormant ports with no consumer (ContextSourceAdapter, EmbeddingProvider, RiskDetector — adapters/types.ts only); reserved tool names under selfExplain: run_overview/trace_node/trace_slice/backtrack/who_wrote/get_value inline, explain_run delegate (AgentBuilder.ts:827-838 — a NEW toolpack tool must join this list).
47
+
48
+ ## Change-impact map
49
+ - **conventions.ts** (STAGE_IDS/SUBFLOW_IDS/INJECTION_KEYS) chart builders that mount by id, ContextRecorder slot attribution, localizer loop-head detection (lib/context-bisect/trajectory.ts:17-33), `stageRole`/`milestoneFor` (Lens contract), BoundaryRecorder. Renaming an id is the whole blast radius.
50
+ - **AgentState** all 8 stages/ files, both builders' mappers, memory-wire STRING-TYPED keys ('runIdentity'/'turnNumber'/… buildAgentChart.ts:177-180 — not refactor-safe), finalizeResult's `reliabilityFail*`/`policyHalt*` reads (rename silently kills the typed errors).
51
+ - **events/** → 65 typed events across 18 domains (counts anti-drift-tested against this file — update BOTH when adding events): ALL_EVENT_TYPES exhaustiveness tests, DomainWildcard hand-list, ~42 importers (recorders, strategies, stream, commentary).
52
+ - **adapters/types.ts LLMMessage/LLMRequest** 62 importers: tool_use round-trip (toolCalls.ts:115-135), wire assembly (callLLM.ts:150-160), providers, cache strategies, security/extractSequence, reliability loop.
53
+ - **Cache** → strategy registration is a MODULE SIDE EFFECT (src/index.ts:15-17); an entry point skipping that import silently falls back to NoOp. Resolved once per Agent at construction (Agent.ts:347).
54
+ - **Injection engine eval semantics** Evaluate stage cursor keystone (buildInjectionEngineSubflow.ts:216-218), Route stage MIRRORS slot filters (:297-307 — keep in sync with buildSystemPromptSlot), read_skill gate (toolCalls.ts:380-400).
55
+ - **Chart shape** (stage order/loop target) → trajectory.ts loop-head bucketing, selfExplain, FlowchartRecorder/RunStepRecorder step synthesis, milestoneFor, the `maxIterations * 2 + 10` engine headroom (Agent.ts:634).
56
+ - **RunnerBase** → all six runners + enable.* strategies + Agent's crash-checkpoint tracker (subscribes to its own dispatcher, Agent.ts:729-752).
57
+
58
+ ## End-to-end trace (agent.run; 1 tool call then final answer; default reactMode 'dynamic')
59
+ build (once): Agent.create → AgentBuilder.build → Agent ctor → initChart(buildChart) (Agent.ts:429; RunnerBase throws on re-init :256) → buildAgentChart assembles: seed → sf-injection-engine → Context selector (`failFast: true`, selects the 3 slot subflows in parallel) → sf-cache → call-llm → route decider → branches tool-calls (pausable, `{loopTo}`) / final (PrepareFinal → memory writes → BreakFinal `$break`, `propagateBreak: true`).
60
+ run: createExecutor (Agent.ts:769): fresh runId + FlowChartExecutor(readTracking 'summary', commitValues 'delta') + enableNarrative + ~12 bridge recorders (causal-evidence ALWAYS inline even under deferred, Agent.ts:814) → installCheckpointTracker (listens iteration_start/end) → executor.run({input, maxIterations: N*2+10}).
61
+ seed#0 (stages/seed.ts:58): history from $getArgs OR consumePendingResumeHistory (:67-72); every emit flows typedEmit $emit → EmitBridge (drops if no listener) → dispatcher, MID-STAGE pre-commit.
62
+ loop body: sf-injection-engine (Gather→Evaluate→Route→Delta; outputMapper `ArrayMergeMode.Replace` — load-bearing) → Context selector fans out sf-system-prompt ‖ sf-messages ‖ sf-tools (each writes its INJECTION_KEYS key; ContextRecorder resolves slot from runtimeStageId) → sf-cache gate → call-llm#N (cacheStrategy.prepareRequest; stream tokens; writes llmLatest*/token counters; reliability retry loop INSIDE this one stage if configured) → route (toolCalls.length && iteration < max ? 'tool-calls' : 'final', stages/route.ts:22-26).
63
+ tool-calls#N (pausable): per call — permission gate → arg validation → credential resolve → tool.execute; PauseRequest → commit partial + RETURN payload = footprintjs pause; result appended role:'tool'; iteration++; `{loopTo: sf-injection-engine}` re-enters the loop (buildAgentChart.ts:473).
64
+ final: prepareFinal sets finalContent + emits turn_end → breakFinal `$break()` returns finalContent → outputMapper bubbles it up → executor resolves → finalizeResult (Agent.ts:884): detectPause → reliabilityFail scan → policyHalt scan → return string. Errors: recoverable + history captured → wrapped in RunCheckpointError (Agent.ts:649-661).
65
+
66
+ ## Backtracking
67
+ Six mechanisms layered on footprintjs (whose transaction/checkpoint machinery lives upstream): **M1** pause/resume — `pauseHere/askHuman` throw PauseRequest inside tool.execute; toolCalls commits history + pausedTool* to scope BEFORE returning the pause payload; scope is the ONLY carrier across the checkpoint. **M2** `resumeOnError` — history-only `AgentRunCheckpoint` (a DIFFERENT type from FlowchartCheckpoint) built from iteration_end events; resume REPLAYS from restored history via the `pendingResumeHistory` side channel. **M3** inline reliability retry — up to 50 attempts inside ONE stage; retry state closure-local, never scope. **M4** chart-level reliability gate — built but UNMOUNTED (buildReliabilityGateChart; editing it changes nothing at runtime). **M5** ReAct loopTo re-entry with `ArrayMergeMode.Replace` guards. **M6** counterfactual replay (context-bisect ablation probes; causal claims only from majority-flip over ≥2 seeded reruns). **M7 (triage surface)** variable-first tools over fp's slice layer: toolpack `backtrack(variable, element?)` (element mode = per-iteration history attribution, exact under the delta default) + `sliceToBacktrackTrace` (structural slice on the atui board — always correlational, every card an upper bound). Deep dive: [.claude/rules/backtracking.md](.claude/rules/backtracking.md).
68
+
69
+ ## Invariants (assumed, not stated)
70
+ - ONE in-flight run per Agent instance (currentRunContext/lastExecutor/pendingResumeHistory are instance fields; concurrent runs corrupt event meta).
71
+ - Chart built once, reference-stable; closures over per-run state must be accessor lambdas (seed.ts:42-49) — direct field capture goes stale on run #2.
72
+ - Subscribe BEFORE run() — listener-presence gating drops (not queues) events at ContextRecorder/EmitBridge/RunnerBase.emit.
73
+ - `arrayMerge: Replace` on EVERY loop-crossed subflow mount (buildAgentChart.ts:284,345,358,382,431,455) — footprintjs default concatenates; omission = injections grow 8→16→24 per iteration.
74
+ - Context selector must stay `failFast: true` (buildAgentChart.ts:329) default allSettled would swallow a throwing required slot and call the LLM half-built.
75
+ - Event payloads must be DETACHED plain data (typedEmit dev-guard) — a live TypedScope proxy breaks deferred-delivery capture and checkpoint serialization.
76
+ - Causal-evidence recorder stays inline even under `observerDelivery:'deferred'` (Agent.ts:808-814) — the memory write stage reads its accumulator mid-run.
77
+ - Tool names + memory ids unique at construction; LLM dispatches by name — a rename is a behavioral change.
78
+
79
+ ## Landmines
80
+ 1. Stale comment at Agent.ts:1053-1054 says the chart is rebuilt per run — it is NOT (eager initChart at :429); providerToolCache IS shared across runs; safety comes only from the Discover stage overwriting `current` each iteration.
81
+ 2. `'classic'` reactMode "caching" is the ABSENCE of re-selection (Context stops picking static slots after turn 1) — "fixing" the selector converts classic into dynamic; classic + skills is broken by design (mid-run activation never reaches cached slots).
82
+ 3. Branch stage ids are BARE (`'final'`, `'tool-calls'`), not the SUBFLOW_IDS prefixed forms — matchers written against SUBFLOW_IDS alone miss real runs (stageRole/milestoneFor deliberately match both).
83
+
84
+ ## Pointers
85
+ - [.claude/rules/backtracking.md](.claude/rules/backtracking.md) 6 mechanisms with step tables + pseudocode
86
+ - [examples/](examples/) — canonical imports (the authority on which subpath exports what) · [src/conventions.ts](src/conventions.ts) — the builder↔recorder protocol
87
+ - Build/test: `npm run build`, `npm test`