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 CHANGED
@@ -25,118 +25,214 @@ npm install agentfootprint agentfootprint-lens
25
25
  ```
26
26
 
27
27
  ```tsx
28
- import { Agent, anthropic } from 'agentfootprint';
29
- import { Lens, useLens } from 'agentfootprint-lens';
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
- const agent = useLens(() =>
33
- Agent.create({ provider: anthropic('claude-sonnet-4') })
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 for={agent} />
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
- That's it. Two lines — `useLens(...)` + `<Lens for={agent} />` — and you get:
53
+ Three lines of wiring — `lensRecorder()`, `recorder.observe(agent)`, `<Lens recorder runner />`.
48
54
 
49
- - A live **Messages** view (everything the LLM saw and said, per turn)
50
- - An **Iteration Strip** (one cell per LLM call, tool call, or decision — scrubbable)
51
- - A **Tool Call Inspector** (args, result, timing for the currently selected step)
52
- - A **Decision Scope Ribbon** (which skill / decision rule was active)
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
- No event wiring, no timeline prop, no snapshot prop. Lens figures it out by watching the runner directly.
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
- As the agent runs, the three columns of Lens fill in live:
67
+ The default view (`view="engineer"`) is one screen:
62
68
 
63
- | Column | Shows |
69
+ | Region | Shows |
64
70
  |---|---|
65
- | **Messages** | The conversation from the agent's perspective — system prompt, user turns, assistant replies, tool results |
66
- | **Iteration Strip** | One row per ReAct loop iteration. Each row lists the LLM call that ran it, the tool calls it picked, and the time each took |
67
- | **Context** | Whichever iteration or tool call is selected — shows the exact prompt the LLM saw, the tools it had available, and what it returned |
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
- When the run finishes, the second tab (**Explainable Trace**) lights up with the full stage-level flowchart — same surface `footprint-explainable-ui` ships, zero extra wiring.
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
- `Lens` doesn't own the agent. Anything can observe it — a Lens, a Datadog exporter, a custom logger, or three of them at once.
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 for={agent} />
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.observe((event) => {
86
- if (event.type === 'llm_end') {
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
- `agent.observe(handler)` is the single subscribe primitive. It returns a `() => void` unsubscribe function. Add as many observers as you want.
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
- Event shape:
110
+ ---
97
111
 
98
- ```ts
99
- type AgentEvent =
100
- | { type: 'turn_start'; userMessage: string }
101
- | { type: 'llm_start'; iteration: number }
102
- | { type: 'llm_end'; iteration: number; content: string; toolCallCount: number; usage?: TokenUsage; latencyMs: number }
103
- | { type: 'tool_start'; toolName: string; args: Record<string, unknown> }
104
- | { type: 'tool_end'; toolName: string; result: { content: string }; latencyMs: number }
105
- | { type: 'token'; content: string } // streaming
106
- | { type: 'turn_end'; content: string; iterations: number };
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
- ## Works with every agentfootprint runner
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
- `<Lens for={...}>` accepts any agentfootprint runner — the same prop works for all of them, and they all light up Lens identically:
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
- // Agent — a ReAct loop
117
- const agent = useLens(() => Agent.create(...).build());
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
- // LLMCall — a single prompt-in, response-out
120
- const caller = useLens(() => LLMCall.create(...).build());
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
- // RAG — retrieve + augment + answer
123
- const rag = useLens(() => RAG.create(...).retriever(...).build());
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
- // Swarm — LLM-routed specialists
126
- const swarm = useLens(() => Swarm.create(...).build());
199
+ ### Replaying a `Trace`
127
200
 
128
- // ...same pattern for FlowChart, Parallel, Conditional
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
- <Lens for={caller} /> // pick whichever
208
+ const trace = JSON.parse(await fs.readFile('run.trace.json', 'utf8'));
209
+ <Replay trace={trace} />
131
210
  ```
132
211
 
133
- One mental model. The runner does the work; Lens watches.
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
- **As of v0.13.0 Lens inherits theme tokens from your app via CSS variables.** Set `--fp-*` (the same names `footprint-explainable-ui` uses) on any parent — Lens picks them up automatically. No `theme=` prop needed; no flash of unstyled content on theme switch.
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 Lens-specific overrides win over shared `--fp-*` design tokens, which win over the built-in defaults.
167
-
168
- ### Light / dark theme switching
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 (when you want Lens to look different from the rest of the app)
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: #0a0e1a; /* darker than the app */
191
- --lens-color-primary: #f59e0b; /* amber accent for Lens chips */
192
- --lens-edge-decision: #ec4899; /* edge color for decision arrows in the graph */
193
- --lens-src-skill: #7c3aed; /* skill-injection chip color */
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
- See `src/react/theme/tokens.ts` for the full token list (surfaces / text / border / accent / 4 edge kinds / 7 injection-source chips / typography).
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
- ### Programmatic override (legacy `theme=` prop)
286
+ ### Server rendering
200
287
 
201
- The old `<Lens theme={tokens} />` API still works for back-compat, but the CSS-variable contract above is the new recommended path — it survives SSR, doesn't reflow on toggle, and themes both Lens and `footprint-explainable-ui` from the same token sheet.
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 vertically (like `<ExplainableShell>` does). Drop it in a splitter, a drawer, or a full-screen tab — no config needed.
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
- ### `useLens(factory)`
303
+ ### `lensRecorder(rootLabel?, options?)`
249
304
 
250
- Memoizes a runner across renders. Call `factory` exactly once on mount; reuses the same instance forever. Works for any agentfootprint runner — `Agent`, `LLMCall`, `RAG`, `Swarm`, `FlowChart`, `Parallel`, `Conditional`.
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
- ```ts
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
- ### `<Lens for={runner} />`
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
- The one-prop integration. Subscribes to the runner's events, watches its snapshot, renders both tabs.
314
+ ### `<Lens>`
261
315
 
262
316
  | Prop | Type | Description |
263
317
  |---|---|---|
264
- | `for` | `Runner` (any agentfootprint runner) | The agent / caller / swarm / etc. to watch. |
265
- | `theme` | `ThemeTokens?` | Optional — defaults to `coolDark`. |
266
- | `appName` | `string?` | Optional brand label in the tab strip. |
267
-
268
- ### `runner.observe(handler)`
269
-
270
- Subscribe to live events. Returns `() => void` (unsubscribe).
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
- const stop = agent.observe((event) => { /* ... */ });
274
- // later:
275
- stop();
354
+ import { structureGraphFromSpec } from 'agentfootprint-lens/core';
355
+
356
+ const graph = structureGraphFromSpec(recording.structure);
276
357
  ```
277
358
 
278
- ### `runner.getSnapshot()`, `runner.getNarrativeEntries()`, `runner.getSpec()`
359
+ `structureGraphFromRunner(runner)` is the same builder from a live runner.
279
360
 
280
- The standard agentfootprint introspection methods. `<Lens for={...}>` reads these automatically. You only call them yourself if you're building a custom UI.
361
+ ### `<WhereFrom>` — walk any value's causes on the one cursor
281
362
 
282
- ### `useLiveTimeline()` (escape hatch)
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
- Returns `{ timeline, ingest, startTurn, reset, builder }`. Use when you want to feed Lens from a non-runner source (replayed logs, server-sent events, etc.).
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. Lens subscribes to those events. Telemetry exporters subscribe to those events. CLI loggers subscribe to those events. Nobody owns the runner; everyone can watch it.
291
-
292
- This is the observer pattern, applied consistently across every agentfootprint runner. The outcome:
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
- - **One line to integrate** — `<Lens for={agent} />`
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 with no conflict
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