agentfootprint-lens 0.28.0 → 0.30.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/README.md +242 -147
- package/dist/{chunk-UBBSNJXC.js → chunk-6UG5QBDF.js} +349 -86
- package/dist/chunk-6UG5QBDF.js.map +1 -0
- package/dist/core.cjs +346 -84
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +3 -1
- package/dist/{index-C4S1EYHe.d.cts → index-626OVHSK.d.cts} +186 -1
- package/dist/{index-C4S1EYHe.d.ts → index-626OVHSK.d.ts} +186 -1
- package/dist/index.cjs +954 -212
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +197 -19
- package/dist/index.d.ts +197 -19
- package/dist/index.js +617 -142
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-UBBSNJXC.js.map +0 -1
package/README.md
CHANGED
|
@@ -25,118 +25,214 @@ npm install agentfootprint agentfootprint-lens
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
28
|
+
import { useMemo } from 'react';
|
|
29
|
+
import { Agent } from 'agentfootprint';
|
|
30
|
+
import { mock } from 'agentfootprint/llm-providers';
|
|
31
|
+
import { Lens, lensRecorder } from 'agentfootprint-lens';
|
|
30
32
|
|
|
31
33
|
export function App() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
// Build the agent and the recorder once, and point the recorder at it.
|
|
35
|
+
const { agent, recorder } = useMemo(() => {
|
|
36
|
+
const agent = Agent.create({ provider: mock({ reply: 'Hi!' }), model: 'mock' })
|
|
34
37
|
.system('You are a helpful assistant.')
|
|
35
|
-
.build()
|
|
36
|
-
|
|
38
|
+
.build();
|
|
39
|
+
const recorder = lensRecorder();
|
|
40
|
+
recorder.observe(agent);
|
|
41
|
+
return { agent, recorder };
|
|
42
|
+
}, []);
|
|
37
43
|
|
|
38
44
|
return (
|
|
39
45
|
<>
|
|
40
|
-
<button onClick={() => agent.run('Hello!')}>Run</button>
|
|
41
|
-
<Lens
|
|
46
|
+
<button onClick={() => agent.run({ message: 'Hello!' })}>Run</button>
|
|
47
|
+
<Lens recorder={recorder} runner={agent} />
|
|
42
48
|
</>
|
|
43
49
|
);
|
|
44
50
|
}
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
Three lines of wiring — `lensRecorder()`, `recorder.observe(agent)`, `<Lens recorder runner />`.
|
|
48
54
|
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- An **Explainable Trace** tab (the full footprintjs stage-level view)
|
|
55
|
+
- `recorder` is what Lens READS: the event log, the run tree, the summary.
|
|
56
|
+
- `runner` is what Lens DRAWS: the composition chart, read from
|
|
57
|
+
`runner.getSpec().buildTimeStructure`. Pass it and the whole chart is visible
|
|
58
|
+
from t=0; leave it out and Lens says so instead of drawing an empty box.
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
Swap `mock(...)` for `anthropic(...)` / `openai(...)` / `ollama(...)` from
|
|
61
|
+
`agentfootprint/llm-providers` — nothing else changes.
|
|
56
62
|
|
|
57
63
|
---
|
|
58
64
|
|
|
59
65
|
## What you actually see
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
The default view (`view="engineer"`) is one screen:
|
|
62
68
|
|
|
63
|
-
|
|
|
69
|
+
| Region | Shows |
|
|
64
70
|
|---|---|
|
|
65
|
-
| **
|
|
66
|
-
| **
|
|
67
|
-
| **
|
|
71
|
+
| **Summary + transport** (top) | status · latency · LLM/tool calls · tokens, and ◀ ▶ ⟳Live with the clickable step strip |
|
|
72
|
+
| **Agents** (left, when a run has 2+) | every Agent / LLMCall instance in the run — click to jump to it |
|
|
73
|
+
| **The chart** (centre) | the composition that ran, lit up as the cursor moves. Click a box to drill into it |
|
|
74
|
+
| **What happened** (right) | the moment-by-moment rail — click any moment to move the one cursor; the focused moment expands to the full detail (prompt, tool args, result, written keys, **Where did this come from?**) |
|
|
75
|
+
| **Events** (bottom) | the raw typed event stream |
|
|
76
|
+
|
|
77
|
+
Two other audiences are one prop away: `view="analyst"` (summary + humanized
|
|
78
|
+
commentary) and `view="user"` (status line + final answer).
|
|
68
79
|
|
|
69
|
-
|
|
80
|
+
Lens watches agentfootprint's typed event stream — `agentfootprint.agent.*`,
|
|
81
|
+
`agentfootprint.stream.*` (`llm_start` / `token` / `tool_start` / …),
|
|
82
|
+
`agentfootprint.context.injected`, `agentfootprint.composition.*` and the rest
|
|
83
|
+
of the 65-type registry. You never wire events yourself; `recorder.observe()`
|
|
84
|
+
subscribes to all of them.
|
|
70
85
|
|
|
71
86
|
---
|
|
72
87
|
|
|
73
88
|
## Multiple watchers, one agent
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```tsx
|
|
78
|
-
const agent = useLens(() => Agent.create(...).build());
|
|
90
|
+
Lens doesn't own the agent. Anything can watch it — a Lens, a telemetry
|
|
91
|
+
exporter, a custom logger, or three at once.
|
|
79
92
|
|
|
93
|
+
```ts
|
|
80
94
|
// Lens in the sidebar
|
|
81
|
-
<Lens
|
|
95
|
+
<Lens recorder={recorder} runner={agent} />
|
|
82
96
|
|
|
83
97
|
// At the same time — ship events to your telemetry backend
|
|
84
98
|
useEffect(() => {
|
|
85
|
-
const stop = agent.
|
|
86
|
-
|
|
87
|
-
telemetry.record('llm.tokens', event.usage?.totalTokens);
|
|
88
|
-
}
|
|
99
|
+
const stop = agent.on('agentfootprint.stream.llm_end', (event) => {
|
|
100
|
+
telemetry.record('llm.tokens', event.payload.usage.input + event.payload.usage.output);
|
|
89
101
|
});
|
|
90
102
|
return stop; // auto-unsubscribe on unmount
|
|
91
103
|
}, [agent]);
|
|
92
104
|
```
|
|
93
105
|
|
|
94
|
-
`
|
|
106
|
+
`runner.on(type, handler)` is the single subscribe primitive. It returns a
|
|
107
|
+
`() => void` unsubscribe. Subscribe to one type, a domain wildcard
|
|
108
|
+
(`'agentfootprint.context.*'`), or `'*'` for everything.
|
|
95
109
|
|
|
96
|
-
|
|
110
|
+
---
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
## Works with every agentfootprint runner
|
|
113
|
+
|
|
114
|
+
The same two props work for all of them:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
const caller = LLMCall.create({ provider, model }).build(); // one prompt in, one answer out
|
|
118
|
+
const agent = Agent.create({ provider, model }).build(); // a ReAct loop
|
|
119
|
+
const chain = Sequence.create({ name: 'Chain' }) // …and Parallel / Conditional / Loop
|
|
120
|
+
.step('draft', caller)
|
|
121
|
+
.step('review', agent)
|
|
122
|
+
.build();
|
|
123
|
+
|
|
124
|
+
const recorder = lensRecorder();
|
|
125
|
+
recorder.observe(caller);
|
|
126
|
+
|
|
127
|
+
<Lens recorder={recorder} runner={caller} />
|
|
107
128
|
```
|
|
108
129
|
|
|
130
|
+
One mental model. The runner does the work; Lens watches.
|
|
131
|
+
|
|
109
132
|
---
|
|
110
133
|
|
|
111
|
-
##
|
|
134
|
+
## Watching a run that already finished
|
|
135
|
+
|
|
136
|
+
A recording is a run you kept. It is exactly THREE things — miss one and one
|
|
137
|
+
surface goes dark, so save all three together.
|
|
112
138
|
|
|
113
|
-
|
|
139
|
+
### Step 1 — record it (in the app that runs the agent)
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const events = [];
|
|
143
|
+
runner.on('*', (e) => events.push(e));
|
|
144
|
+
|
|
145
|
+
await runner.run({ message });
|
|
146
|
+
|
|
147
|
+
const recording = {
|
|
148
|
+
events, // 1. the timeline
|
|
149
|
+
snapshot: runner.getLastSnapshot(), // 2. state + commit log + every recorder's data
|
|
150
|
+
structure: runner.getSpec().buildTimeStructure, // 3. THE CHART. Nothing else can draw it.
|
|
151
|
+
};
|
|
152
|
+
fs.writeFileSync('run.json', JSON.stringify(recording));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`structure` is the one piece a run does not leave behind on its own —
|
|
156
|
+
`getSnapshot()` never includes it — which is why so many stored runs replay
|
|
157
|
+
without a chart.
|
|
158
|
+
|
|
159
|
+
### Step 2 — render it
|
|
114
160
|
|
|
115
161
|
```tsx
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
import { useMemo } from 'react';
|
|
163
|
+
import { observeRecording, Lens } from 'agentfootprint-lens';
|
|
164
|
+
|
|
165
|
+
// One call = one replay: it builds a recorder and walks the whole event log.
|
|
166
|
+
// Keep it out of the render body.
|
|
167
|
+
const observed = useMemo(() => observeRecording(recording), [recording]);
|
|
168
|
+
|
|
169
|
+
return <Lens recorder={observed.recorder} runner={observed.runner} />;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Nothing re-runs, no model is called, no network is touched. Anything the
|
|
173
|
+
recording could not give back, Lens states on screen — you do not have to
|
|
174
|
+
render the return values yourself.
|
|
118
175
|
|
|
119
|
-
|
|
120
|
-
|
|
176
|
+
| piece | where it comes from | what it buys |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `events` | `runner.on('*', …)` collected during the run | the messages, the moments rail, the commentary, the summary |
|
|
179
|
+
| `snapshot` | the run's footprintjs `getSnapshot()` | the commit axis, `<WhereFrom>`'s provenance, and every attached recorder's data |
|
|
180
|
+
| `structure` | `runner.getSpec().buildTimeStructure` | the chart — the composition that actually ran |
|
|
181
|
+
|
|
182
|
+
Recordings frozen as `{ snapshot, events, blueprint }` work as-is: `blueprint`
|
|
183
|
+
is read when `structure` is absent.
|
|
184
|
+
|
|
185
|
+
**The step strip needs one more thing at RECORD time:** attach agentfootprint's
|
|
186
|
+
`boundaryRecorder` (with commit tracking) while the run happens. Its snapshot
|
|
187
|
+
entry stamps every subflow entry/exit with the commit index it crossed at, and
|
|
188
|
+
that is what the strip is indexed by. A recording without it is still fully
|
|
189
|
+
watchable — the strip stays quiet and says so. Lens will not guess the ranges
|
|
190
|
+
from the commit log: the log cannot say *when* a boundary opened (a fork's
|
|
191
|
+
branches all open at a moment it has no row for), and guessing produced 20 stops
|
|
192
|
+
on a run that had 17.
|
|
121
193
|
|
|
122
|
-
|
|
123
|
-
|
|
194
|
+
`observeRecording` also returns the counts, for code that wants to check before
|
|
195
|
+
rendering: `chart` (`'drawn' | 'absent'`), `eventsReplayed`, `eventsSkipped`,
|
|
196
|
+
`boundaryEvents`, `boundaryRanges`, and `notes` — one line per thing the
|
|
197
|
+
recording carried that could not be READ, as opposed to was not there.
|
|
124
198
|
|
|
125
|
-
|
|
126
|
-
const swarm = useLens(() => Swarm.create(...).build());
|
|
199
|
+
### Replaying a `Trace`
|
|
127
200
|
|
|
128
|
-
|
|
201
|
+
agentfootprint's `enable.localObservability().getTrace()` produces a `Trace` —
|
|
202
|
+
a different transport for the same idea. `<Replay trace={trace} />` adapts it
|
|
203
|
+
onto the same path:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import { Replay } from 'agentfootprint-lens';
|
|
129
207
|
|
|
130
|
-
|
|
208
|
+
const trace = JSON.parse(await fs.readFile('run.trace.json', 'utf8'));
|
|
209
|
+
<Replay trace={trace} />
|
|
131
210
|
```
|
|
132
211
|
|
|
133
|
-
|
|
212
|
+
A `Trace` carries the boundary log rather than the typed event log, so it
|
|
213
|
+
replays as chart + step strip + detail, with the commentary rail quiet — and
|
|
214
|
+
Lens says so. For the full surface, record `{ snapshot, events, structure }` and
|
|
215
|
+
use `observeRecording`.
|
|
134
216
|
|
|
135
217
|
---
|
|
136
218
|
|
|
137
219
|
## Theming
|
|
138
220
|
|
|
139
|
-
**
|
|
221
|
+
**Lens inherits theme tokens from your app via CSS variables.** Set `--fp-*`
|
|
222
|
+
(the same names `footprint-explainable-ui` uses) on any parent and Lens picks
|
|
223
|
+
them up — no `theme=` prop needed, no flash of unstyled content on a theme
|
|
224
|
+
switch. Lens's stylesheet ships with the library and injects itself; there is
|
|
225
|
+
no CSS file to import.
|
|
226
|
+
|
|
227
|
+
### The one-line switch
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
<Lens recorder={recorder} runner={agent} theme={{ mode: 'light' }} />
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`mode` applies eui's full light/dark preset at the Lens root — so the chart, the
|
|
234
|
+
panels, the edge colours and the injection-source chips all follow, in all three
|
|
235
|
+
views, from that one word.
|
|
140
236
|
|
|
141
237
|
### The token contract — set these on `:root` (or any parent of `<Lens>`)
|
|
142
238
|
|
|
@@ -146,6 +242,7 @@ One mental model. The runner does the work; Lens watches.
|
|
|
146
242
|
--fp-bg-primary: #0f172a;
|
|
147
243
|
--fp-bg-secondary: #1e293b;
|
|
148
244
|
--fp-bg-tertiary: #334155;
|
|
245
|
+
--fp-bg-elevated: #1e293b; /* the card fill behind Lens's own panels */
|
|
149
246
|
|
|
150
247
|
/* Text */
|
|
151
248
|
--fp-text-primary: #f8fafc;
|
|
@@ -163,138 +260,136 @@ One mental model. The runner does the work; Lens watches.
|
|
|
163
260
|
}
|
|
164
261
|
```
|
|
165
262
|
|
|
166
|
-
Resolution order per token: **`--lens-X` → `--fp-X` → hardcoded fallback**. So
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
If your app already toggles theme by mutating CSS variables on `:root`, `body`, or a wrapper, Lens follows automatically with no extra wiring:
|
|
171
|
-
|
|
172
|
-
```tsx
|
|
173
|
-
function App() {
|
|
174
|
-
const [dark, setDark] = useState(true);
|
|
175
|
-
return (
|
|
176
|
-
<div data-theme={dark ? 'dark' : 'light'}>
|
|
177
|
-
{/* your existing :root[data-theme=dark] { --fp-* … } CSS */}
|
|
178
|
-
<Lens for={agent} />
|
|
179
|
-
</div>
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
```
|
|
263
|
+
Resolution order per token: **`--lens-X` → `--fp-X` → hardcoded fallback**. So
|
|
264
|
+
Lens-specific overrides win over shared `--fp-*` design tokens, which win over
|
|
265
|
+
the built-in defaults. `theme={{ mode }}` writes into the `--fp-*` tier only —
|
|
266
|
+
your `--lens-*` still wins.
|
|
183
267
|
|
|
184
|
-
### Lens-only overrides
|
|
185
|
-
|
|
186
|
-
Set `--lens-*` on a parent of `<Lens>` only:
|
|
268
|
+
### Lens-only overrides
|
|
187
269
|
|
|
188
270
|
```css
|
|
189
271
|
.my-lens-container {
|
|
190
|
-
--lens-bg-primary:
|
|
191
|
-
--lens-color-primary:
|
|
192
|
-
--lens-edge-decision:
|
|
193
|
-
--lens-src-skill:
|
|
272
|
+
--lens-bg-primary: #0a0e1a; /* darker than the app */
|
|
273
|
+
--lens-color-primary: #f59e0b; /* amber accent for Lens chips */
|
|
274
|
+
--lens-edge-decision: #ec4899; /* decision arrows in the graph */
|
|
275
|
+
--lens-src-skill: #7c3aed; /* skill-injection chip */
|
|
276
|
+
--lens-agent-color-0: #22d3ee; /* first agent's swatch in the legend */
|
|
194
277
|
}
|
|
195
278
|
```
|
|
196
279
|
|
|
197
|
-
|
|
280
|
+
Every token has a built-in value, so nothing is ever unpainted. See
|
|
281
|
+
`src/react/theme/tokens.ts` for the full list (surfaces / text / border /
|
|
282
|
+
accent / 4 edge kinds / 7 injection-source chips / 8 agent swatches /
|
|
283
|
+
typography), all of it exported as `T`, `RAW_DEFAULTS`, `AGENT_COLORS` and
|
|
284
|
+
`MODE_PALETTES`.
|
|
198
285
|
|
|
199
|
-
###
|
|
286
|
+
### Server rendering
|
|
200
287
|
|
|
201
|
-
|
|
288
|
+
`LENS_STYLESHEET` is the stylesheet as a string — put it in your own `<style>`
|
|
289
|
+
if a strict CSP blocks the automatic injection.
|
|
202
290
|
|
|
203
291
|
---
|
|
204
292
|
|
|
205
293
|
## Responsive
|
|
206
294
|
|
|
207
|
-
Lens resizes to whatever space you give it. Below ~640px wide it stacks panels
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
## Escape hatches
|
|
212
|
-
|
|
213
|
-
If you want to manage the timeline yourself (custom ingestion, recording to a file, replaying a stored run), the explicit path is still available:
|
|
214
|
-
|
|
215
|
-
```tsx
|
|
216
|
-
import { Lens, useLiveTimeline } from 'agentfootprint-lens';
|
|
217
|
-
|
|
218
|
-
const lens = useLiveTimeline();
|
|
219
|
-
|
|
220
|
-
// You control ingestion
|
|
221
|
-
for (const event of storedEvents) lens.ingest(event);
|
|
222
|
-
|
|
223
|
-
<Lens
|
|
224
|
-
timeline={lens.timeline}
|
|
225
|
-
runtimeSnapshot={storedSnapshot}
|
|
226
|
-
/>
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
---
|
|
230
|
-
|
|
231
|
-
## Recorder pattern (power users)
|
|
232
|
-
|
|
233
|
-
For advanced observability — multiple exporters, buffering, filtering before dispatch — agentfootprint's recorder system is still there:
|
|
234
|
-
|
|
235
|
-
```ts
|
|
236
|
-
import { createStreamEventRecorder } from 'agentfootprint';
|
|
237
|
-
|
|
238
|
-
const myRec = createStreamEventRecorder(myHandler, 'my-telemetry');
|
|
239
|
-
const agent = Agent.create(...).recorder(myRec).build();
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
`<Lens for={...}>` is just sugar over this internally — the recorder you'd write for Datadog is the same shape Lens uses.
|
|
295
|
+
Lens resizes to whatever space you give it. Below ~640px wide it stacks panels
|
|
296
|
+
vertically (like `<ExplainableShell>` does). Drop it in a splitter, a drawer, or
|
|
297
|
+
a full-screen tab — no config needed.
|
|
243
298
|
|
|
244
299
|
---
|
|
245
300
|
|
|
246
301
|
## API reference
|
|
247
302
|
|
|
248
|
-
### `
|
|
303
|
+
### `lensRecorder(rootLabel?, options?)`
|
|
249
304
|
|
|
250
|
-
|
|
305
|
+
Builds a `LensRecorder`. `options.maxEvents` caps the event log (default 50 000,
|
|
306
|
+
oldest evicted, counted in `getDiagnostics().droppedEvents` — never silent);
|
|
307
|
+
`options.debug` forces the dev-mode console diagnostics on or off.
|
|
251
308
|
|
|
252
|
-
|
|
253
|
-
const agent = useLens(() => Agent.create(...).build());
|
|
254
|
-
const caller = useLens(() => LLMCall.create(...).build());
|
|
255
|
-
const rag = useLens(() => RAG.create(...).build());
|
|
256
|
-
```
|
|
309
|
+
### `recorder.observe(runner)`
|
|
257
310
|
|
|
258
|
-
|
|
311
|
+
Subscribe to a runner's typed dispatcher, its recorder channel and its step
|
|
312
|
+
graph in one call. Returns an unsubscribe. Call it once per run.
|
|
259
313
|
|
|
260
|
-
|
|
314
|
+
### `<Lens>`
|
|
261
315
|
|
|
262
316
|
| Prop | Type | Description |
|
|
263
317
|
|---|---|---|
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
318
|
+
| `recorder` | `LensRecorder` | **Required.** What Lens reads. |
|
|
319
|
+
| `runner` | `Runner?` | The runner (or `observeRecording`'s) whose build-time structure Lens draws. Omit and the chart region says what is missing. |
|
|
320
|
+
| `theme` | `LensTheme?` | `{ mode?: 'dark' \| 'light', ground?, visited?, current? }`. |
|
|
321
|
+
| `view` | `'engineer' \| 'analyst' \| 'user'` | Default `'engineer'`. |
|
|
322
|
+
| `stepStrip` | `boolean?` | Show the clickable step strip. Default `true`. |
|
|
323
|
+
| `showSummary` | `boolean?` | Show the status/metrics bar. Default `true`. |
|
|
324
|
+
| `appName` | `string?` | The name used as the active actor in every commentary line. Default `'Chatbot'`. |
|
|
325
|
+
| `humanizer` | `Humanizer?` | Override the commentary function entirely. |
|
|
326
|
+
| `commentaryTemplates` | `Partial<CommentaryTemplates>?` | Override individual lines (locale, brand voice). |
|
|
327
|
+
| `chart` | `LensFlowProps['chart']?` | Render YOUR graph instead of the derived one. |
|
|
328
|
+
| `stepGraph` | `StepGraph?` | Bring your own step graph; by default Lens uses the recorder's. |
|
|
329
|
+
| `toolChoice` | `ToolChoiceSource?` | Mount the per-iteration tool-choice panel. |
|
|
330
|
+
|
|
331
|
+
### `observeRecording(recording, options?)`
|
|
332
|
+
|
|
333
|
+
The offline twin of `recorder.observe(runner)`. Takes
|
|
334
|
+
`{ snapshot, events, structure }` (or `blueprint`) — whatever the recording
|
|
335
|
+
carries — and returns `{ recorder, runner, chart, eventsReplayed, eventsSkipped,
|
|
336
|
+
boundaryEvents, boundaryRanges, notes }`. Hand `recorder` and `runner` straight
|
|
337
|
+
to `<Lens>`. See
|
|
338
|
+
[Watching a run that already finished](#watching-a-run-that-already-finished).
|
|
339
|
+
|
|
340
|
+
### `<Replay trace={trace} />`
|
|
341
|
+
|
|
342
|
+
The `Trace`-shaped door into the same replay path. See
|
|
343
|
+
[Replaying a `Trace`](#replaying-a-trace).
|
|
344
|
+
|
|
345
|
+
### `structureGraphFromRunner(runner)` / `structureGraphFromSpec(structure)`
|
|
346
|
+
|
|
347
|
+
The runner → chart adapter, exported from `agentfootprint-lens/core`. It walks a
|
|
348
|
+
footprintjs build-time spec into an `explainable-ui` `TraceGraph` whose node ids
|
|
349
|
+
ARE the real runtime stage ids, so a runtime overlay lights the executed path.
|
|
350
|
+
`<Lens runner>` calls it for you; call it yourself to render the chart in your
|
|
351
|
+
own shell, or to feed `<ExplainableShell traceGraph={…} />`:
|
|
271
352
|
|
|
272
353
|
```ts
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
354
|
+
import { structureGraphFromSpec } from 'agentfootprint-lens/core';
|
|
355
|
+
|
|
356
|
+
const graph = structureGraphFromSpec(recording.structure);
|
|
276
357
|
```
|
|
277
358
|
|
|
278
|
-
|
|
359
|
+
`structureGraphFromRunner(runner)` is the same builder from a live runner.
|
|
279
360
|
|
|
280
|
-
|
|
361
|
+
### `<WhereFrom>` — walk any value's causes on the one cursor
|
|
281
362
|
|
|
282
|
-
|
|
363
|
+
In the engineer view's detail panel, the cursor stage's written keys render as
|
|
364
|
+
chips; picking one shows the backward slice that produced its value (footprintjs
|
|
365
|
+
`sliceForKey` — the same query the `backtrack` LLM tool runs). **◀ Walk the
|
|
366
|
+
causes** freezes that slice as reverse-time stops and steps the ONE cursor
|
|
367
|
+
through them ("◀ earlier cause / toward result ▶") — both parents of a fork are
|
|
368
|
+
always visited, the chart cone follows the walk, and **[Copy story]** emits the
|
|
369
|
+
exact `formatSlice` text the LLM tool returns. Honest absence stays honest:
|
|
370
|
+
"never written — initial state / args / a closure", and reads-off runs say
|
|
371
|
+
"unknowable, not absent".
|
|
283
372
|
|
|
284
|
-
|
|
373
|
+
### Headless core
|
|
374
|
+
|
|
375
|
+
`agentfootprint-lens/core` is React-free: `LensRecorder`, `ChangeNotifier`,
|
|
376
|
+
`observeRecording`, the selectors and the graph adapters. Build a Vue / Angular
|
|
377
|
+
/ CLI view on the same primitives — see `ChangeNotifier`'s JSDoc for adapter
|
|
378
|
+
snippets.
|
|
285
379
|
|
|
286
380
|
---
|
|
287
381
|
|
|
288
382
|
## Why this design
|
|
289
383
|
|
|
290
|
-
**The runner is the single source of truth.** Agents fire events as they work.
|
|
291
|
-
|
|
292
|
-
|
|
384
|
+
**The runner is the single source of truth.** Agents fire events as they work.
|
|
385
|
+
Lens subscribes to those events. Telemetry exporters subscribe to those events.
|
|
386
|
+
CLI loggers subscribe to those events. Nobody owns the runner; everyone can
|
|
387
|
+
watch it.
|
|
293
388
|
|
|
294
|
-
- **
|
|
389
|
+
- **Three lines to integrate** — `lensRecorder()` + `observe()` + `<Lens>`
|
|
295
390
|
- **Zero coupling** — the agent doesn't know Lens exists
|
|
296
|
-
- **Composable** — Lens + your telemetry + your logger all watch the same agent
|
|
297
|
-
- **Uniform** — any runner works with any observer
|
|
391
|
+
- **Composable** — Lens + your telemetry + your logger all watch the same agent
|
|
392
|
+
- **Uniform** — any runner works with any observer, live or replayed
|
|
298
393
|
|
|
299
394
|
---
|
|
300
395
|
|