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