agentfootprint 7.0.0 → 7.1.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.
- package/CLAUDE.md +83 -622
- package/dist/core/agent/AgentBuilder.js +1 -1
- package/dist/core/agent/AgentBuilder.js.map +1 -1
- package/dist/debug.js +2 -1
- package/dist/debug.js.map +1 -1
- package/dist/esm/core/agent/AgentBuilder.js +1 -1
- package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
- package/dist/esm/debug.d.ts +1 -1
- package/dist/esm/debug.js +1 -1
- package/dist/esm/debug.js.map +1 -1
- package/dist/esm/lib/context-bisect/index.d.ts +1 -0
- package/dist/esm/lib/context-bisect/index.js +3 -0
- package/dist/esm/lib/context-bisect/index.js.map +1 -1
- package/dist/esm/lib/context-bisect/sliceToBacktrackTrace.d.ts +52 -0
- package/dist/esm/lib/context-bisect/sliceToBacktrackTrace.js +150 -0
- package/dist/esm/lib/context-bisect/sliceToBacktrackTrace.js.map +1 -0
- package/dist/esm/lib/trace-toolpack/traceToolpack.d.ts +1 -0
- package/dist/esm/lib/trace-toolpack/traceToolpack.js +111 -0
- package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
- package/dist/lib/context-bisect/index.js +5 -1
- package/dist/lib/context-bisect/index.js.map +1 -1
- package/dist/lib/context-bisect/sliceToBacktrackTrace.js +154 -0
- package/dist/lib/context-bisect/sliceToBacktrackTrace.js.map +1 -0
- package/dist/lib/trace-toolpack/traceToolpack.js +111 -0
- package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
- package/dist/types/debug.d.ts +1 -1
- package/dist/types/debug.d.ts.map +1 -1
- package/dist/types/lib/context-bisect/index.d.ts +1 -0
- package/dist/types/lib/context-bisect/index.d.ts.map +1 -1
- package/dist/types/lib/context-bisect/sliceToBacktrackTrace.d.ts +53 -0
- package/dist/types/lib/context-bisect/sliceToBacktrackTrace.d.ts.map +1 -0
- package/dist/types/lib/trace-toolpack/traceToolpack.d.ts +1 -0
- package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
- package/package.json +4 -4
package/CLAUDE.md
CHANGED
|
@@ -1,626 +1,87 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- analyzed-at: 5a5b7cb @ 2026-07-02 | model: fable-5 -->
|
|
2
|
+
# agentfootprint — feature-work map
|
|
2
3
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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?)`), 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`
|