agentfootprint-lens 0.4.0 → 0.6.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
@@ -1,227 +1,260 @@
1
1
  # agentfootprint-lens
2
2
 
3
- > **See through your agent's decisions.**
3
+ > **See the context engineering as it happens.**
4
4
  >
5
- > React components for debugging agents built on [`agentfootprint`](https://www.npmjs.com/package/agentfootprint): messages, prompt composition, tool calls, decision scope, and costin one scrub-able timeline.
5
+ > React components for watching agents built on [`agentfootprint`](https://www.npmjs.com/package/agentfootprint). Every injection into the Agent's slots (RAG, Memory, Skills, Instructions, Tools) is tagged inline students and engineers see exactly what was put into the prompt, by whom, on which iteration. No hidden abstractions.
6
+
7
+ ---
8
+
9
+ ### The pitch
10
+
11
+ agentfootprint = **2 primitives (LLM, Agent) + 3 compositions (Sequence, Parallel, Conditional) + N patterns (ReAct, Reflexion, Tree-of-Thoughts...) + cross-cutting context engineering.** Lens is the surface that makes the context engineering visible — not as a "RAG view" or a "Memory view," but as tagged injections inside the ONE Agent card. That's the whole pedagogy.
6
12
 
7
13
  [![npm version](https://img.shields.io/npm/v/agentfootprint-lens.svg)](https://www.npmjs.com/package/agentfootprint-lens)
8
14
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
9
15
 
10
16
  ---
11
17
 
12
- ## Why this exists
18
+ ## 30-second quick start
13
19
 
14
- `footprint-explainable-ui` is the debugger for **pipelines** — stages, flowchart topology, commit log, data lineage. It's perfect for the person writing a footprintjs tool.
20
+ ```bash
21
+ npm install agentfootprint agentfootprint-lens
22
+ ```
15
23
 
16
- But an **agent** isn't a pipeline — it's a loop of (LLM call → parse → tool calls → repeat), steered by a decision scope and gated by skills. The questions an agent developer asks are different:
24
+ ```tsx
25
+ import { Agent, anthropic } from 'agentfootprint';
26
+ import { Lens, useLens } from 'agentfootprint-lens';
17
27
 
18
- - "What prompt did the LLM actually see at iter 4?"
19
- - "Why did the agent pick **this** tool over that one?"
20
- - "Which skill was active when it went sideways?"
21
- - "How many tokens did this turn cost me?"
22
- - "What changed between turn 1 and the follow-up?"
28
+ export function App() {
29
+ const agent = useLens(() =>
30
+ Agent.create({ provider: anthropic('claude-sonnet-4') })
31
+ .system('You are a helpful assistant.')
32
+ .build()
33
+ );
23
34
 
24
- **Lens** answers those. It reads `agent.getSnapshot()`, parses it into an agent-shaped timeline, and renders the agent-native surfaces:
35
+ return (
36
+ <>
37
+ <button onClick={() => agent.run('Hello!')}>Run</button>
38
+ <Lens for={agent} />
39
+ </>
40
+ );
41
+ }
42
+ ```
25
43
 
26
- 1. **Messages Panel**chat bubbles with expandable tool-call cards, turn boundaries, iteration markers.
27
- 2. **Iteration Strip** — horizontal ribbon of every LLM call in the run. Click to jump.
28
- 3. **Tool Call Inspector** — flat sidebar of every tool invocation across all turns.
44
+ That's it. Two lines`useLens(...)` + `<Lens for={agent} />` and you get:
29
45
 
30
- (phase-2: Prompt Composer, Decision Scope Ribbon, Cost & Latency Attribution.)
46
+ - A live **Messages** view (everything the LLM saw and said, per turn)
47
+ - An **Iteration Strip** (one cell per LLM call, tool call, or decision — scrubbable)
48
+ - A **Tool Call Inspector** (args, result, timing for the currently selected step)
49
+ - A **Decision Scope Ribbon** (which skill / decision rule was active)
50
+ - An **Explainable Trace** tab (the full footprintjs stage-level view)
31
51
 
32
- For tool-internal debugging (what did this tool's footprintjs flowchart actually do?), Lens composes [`footprint-explainable-ui`](https://www.npmjs.com/package/footprint-explainable-ui) as a drill-in drawer. The two libraries are siblings, not competitors.
52
+ No event wiring, no timeline prop, no snapshot prop. Lens figures it out by watching the runner directly.
33
53
 
34
54
  ---
35
55
 
36
- ## Install
56
+ ## What you actually see
37
57
 
38
- ```bash
39
- npm install agentfootprint-lens footprint-explainable-ui
40
- ```
58
+ As the agent runs, the three columns of Lens fill in live:
41
59
 
42
- Peer deps: React 18+, `footprint-explainable-ui@^0.18.0`.
60
+ | Column | Shows |
61
+ |---|---|
62
+ | **Messages** | The conversation from the agent's perspective — system prompt, user turns, assistant replies, tool results |
63
+ | **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 |
64
+ | **Context** | Whichever iteration or tool call is selected — shows the exact prompt the LLM saw, the tools it had available, and what it returned |
65
+
66
+ 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.
43
67
 
44
68
  ---
45
69
 
46
- ## Quick start
70
+ ## Multiple watchers, one agent
71
+
72
+ `Lens` doesn't own the agent. Anything can observe it — a Lens, a Datadog exporter, a custom logger, or three of them at once.
47
73
 
48
74
  ```tsx
49
- import { AgentLens } from 'agentfootprint-lens';
50
- import { FootprintTheme, coolDark, coolLight } from 'footprint-explainable-ui';
51
- import { Agent, anthropic } from 'agentfootprint';
52
- import { useState, useEffect } from 'react';
75
+ const agent = useLens(() => Agent.create(...).build());
76
+
77
+ // Lens in the sidebar
78
+ <Lens for={agent} />
79
+
80
+ // At the same time — ship events to your telemetry backend
81
+ useEffect(() => {
82
+ const stop = agent.observe((event) => {
83
+ if (event.type === 'llm_end') {
84
+ telemetry.record('llm.tokens', event.usage?.totalTokens);
85
+ }
86
+ });
87
+ return stop; // auto-unsubscribe on unmount
88
+ }, [agent]);
89
+ ```
53
90
 
54
- export function MyApp() {
55
- const [dark, setDark] = useState(true);
56
- const [snapshot, setSnapshot] = useState(null);
91
+ `agent.observe(handler)` is the single subscribe primitive. It returns a `() => void` unsubscribe function. Add as many observers as you want.
57
92
 
58
- useEffect(() => {
59
- const agent = Agent.create({ provider: anthropic('claude-haiku-4-5') })
60
- .system('You are a helpful agent.')
61
- .build();
62
- agent.run('What time is it?').then(() => setSnapshot(agent.getSnapshot()));
63
- }, []);
93
+ Event shape:
64
94
 
65
- return (
66
- <FootprintTheme tokens={dark ? coolDark : coolLight}>
67
- <div style={{ height: '100vh' }}>
68
- <AgentLens runtimeSnapshot={snapshot} />
69
- </div>
70
- </FootprintTheme>
71
- );
72
- }
95
+ ```ts
96
+ type AgentEvent =
97
+ | { type: 'turn_start'; userMessage: string }
98
+ | { type: 'llm_start'; iteration: number }
99
+ | { type: 'llm_end'; iteration: number; content: string; toolCallCount: number; usage?: TokenUsage; latencyMs: number }
100
+ | { type: 'tool_start'; toolName: string; args: Record<string, unknown> }
101
+ | { type: 'tool_end'; toolName: string; result: { content: string }; latencyMs: number }
102
+ | { type: 'token'; content: string } // streaming
103
+ | { type: 'turn_end'; content: string; iterations: number };
73
104
  ```
74
105
 
75
- That's it. Lens reads the same `FootprintTheme` context explainable-ui reads, so **the consumer owns theming** — flip `coolDark` ↔ `coolLight` at the app root and both the agent view (Lens) and any drill-in trace view (explainable-ui) follow together.
76
-
77
106
  ---
78
107
 
79
- ## Composition: drill-in to a tool's flowchart
108
+ ## Works with every agentfootprint runner
80
109
 
81
- Each tool in agentfootprint is typically a `flowChart<State>` underneath. When a user wants to debug *what that tool did internally*, open an explainable-ui drawer:
110
+ `<Lens for={...}>` accepts any agentfootprint runner the same prop works for all of them, and they all light up Lens identically:
82
111
 
83
112
  ```tsx
84
- import { AgentLens, type AgentToolInvocation } from 'agentfootprint-lens';
85
- import { ExplainableShell } from 'footprint-explainable-ui';
113
+ // Agent a ReAct loop
114
+ const agent = useLens(() => Agent.create(...).build());
86
115
 
87
- function Shell({ snapshot }) {
88
- const [selected, setSelected] = useState<AgentToolInvocation | null>(null);
89
- return (
90
- <>
91
- <AgentLens runtimeSnapshot={snapshot} onToolCallClick={setSelected} />
92
- {selected && (
93
- <Drawer onClose={() => setSelected(null)}>
94
- <ExplainableShell
95
- runtimeSnapshot={extractToolSubSnapshot(snapshot, selected.id)}
96
- title={`${selected.name} · internals`}
97
- />
98
- </Drawer>
99
- )}
100
- </>
101
- );
102
- }
116
+ // LLMCall a single prompt-in, response-out
117
+ const caller = useLens(() => LLMCall.create(...).build());
118
+
119
+ // RAG — retrieve + augment + answer
120
+ const rag = useLens(() => RAG.create(...).retriever(...).build());
121
+
122
+ // Swarm LLM-routed specialists
123
+ const swarm = useLens(() => Swarm.create(...).build());
124
+
125
+ // ...same pattern for FlowChart, Parallel, Conditional
126
+
127
+ <Lens for={caller} /> // pick whichever
103
128
  ```
104
129
 
105
- Lens says "this is what the agent did." explainable-ui says "this is what each tool's flowchart did." Clean separation, no duplication.
130
+ One mental model. The runner does the work; Lens watches.
106
131
 
107
132
  ---
108
133
 
109
- ## API surface
134
+ ## Theming
110
135
 
111
- ### `<AgentLens>` the one-stop shell
136
+ Lens inherits from `footprint-explainable-ui`'s theme system. Two built-in presets, or bring your own:
112
137
 
113
138
  ```tsx
114
- <AgentLens
115
- runtimeSnapshot={agent.getSnapshot()}
116
- onToolCallClick={(invocation) => /* open drill-in */}
139
+ import { coolDark, coolLight } from 'footprint-explainable-ui';
140
+ import { Lens } from 'agentfootprint-lens';
141
+
142
+ <Lens for={agent} theme={isDark ? coolDark : coolLight} />
143
+ ```
144
+
145
+ Pass any `ThemeTokens` object. CSS vars work too — handy if your app already flips theme at the `:root` level:
146
+
147
+ ```tsx
148
+ <Lens
149
+ for={agent}
150
+ theme={{
151
+ colors: {
152
+ bgPrimary: 'var(--my-bg)',
153
+ textPrimary: 'var(--my-fg)',
154
+ // …
155
+ },
156
+ }}
117
157
  />
118
158
  ```
119
159
 
120
- Props:
121
- - `runtimeSnapshot` (any | null) — raw output of `agent.getSnapshot()`. Null renders an empty state.
122
- - `timeline` (`AgentTimeline`) — pre-parsed timeline, overrides `runtimeSnapshot`. Useful for sharing across multiple Lens instances.
123
- - `systemPrompt` (string) — override the system-prompt preview in MessagesPanel. Auto-derived from snapshot otherwise.
124
- - `onToolCallClick` ((`AgentToolInvocation`) => void) — fires when any tool-call card is clicked.
160
+ ---
125
161
 
126
- ### Individual panels (composable)
162
+ ## Responsive
127
163
 
128
- - `<MessagesPanel timeline onToolCallClick systemPrompt />`
129
- - `<IterationStrip timeline selectedKey onSelect />`
130
- - `<ToolCallInspector timeline selectedId onSelect />`
164
+ 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.
131
165
 
132
- ### Adapter
166
+ ---
133
167
 
134
- - `fromAgentSnapshot(runtimeSnapshot) → AgentTimeline` — pure function. Useful for non-UI consumers (eval scripts, exporters).
168
+ ## Escape hatches
135
169
 
136
- ### Theme
170
+ If you want to manage the timeline yourself (custom ingestion, recording to a file, replaying a stored run), the explicit path is still available:
137
171
 
138
- Lens reads `useFootprintTheme()` (from explainable-ui). Wrap your tree in `<FootprintTheme tokens={...}>` and Lens follows. No Lens-specific theme API.
172
+ ```tsx
173
+ import { Lens, useLiveTimeline } from 'agentfootprint-lens';
174
+
175
+ const lens = useLiveTimeline();
176
+
177
+ // You control ingestion
178
+ for (const event of storedEvents) lens.ingest(event);
179
+
180
+ <Lens
181
+ timeline={lens.timeline}
182
+ runtimeSnapshot={storedSnapshot}
183
+ />
184
+ ```
139
185
 
140
186
  ---
141
187
 
142
- ## Data model
188
+ ## Recorder pattern (power users)
143
189
 
144
- `AgentTimeline` is what Lens renders against. `fromAgentSnapshot` derives it from the raw runtime snapshot:
190
+ For advanced observability multiple exporters, buffering, filtering before dispatch agentfootprint's recorder system is still there:
145
191
 
146
192
  ```ts
147
- interface AgentTimeline {
148
- turns: AgentTurn[]; // one per agent.run() call
149
- messages: AgentMessage[]; // full flat conversation
150
- tools: AgentToolInvocation[]; // every tool call, across turns
151
- finalDecision: Record<string, unknown>;
152
- rawSnapshot: unknown; // escape hatch
153
- }
193
+ import { createStreamEventRecorder } from 'agentfootprint';
154
194
 
155
- interface AgentTurn {
156
- index: number;
157
- userPrompt: string;
158
- iterations: AgentIteration[];
159
- finalContent: string;
160
- totalInputTokens: number;
161
- totalOutputTokens: number;
162
- totalDurationMs: number;
163
- }
195
+ const myRec = createStreamEventRecorder(myHandler, 'my-telemetry');
196
+ const agent = Agent.create(...).recorder(myRec).build();
197
+ ```
164
198
 
165
- interface AgentIteration {
166
- index: number;
167
- model?: string;
168
- inputTokens?: number;
169
- outputTokens?: number;
170
- durationMs?: number;
171
- stopReason?: string;
172
- assistantContent: string;
173
- toolCalls: AgentToolInvocation[];
174
- decisionAtStart: Record<string, unknown>;
175
- matchedInstructions?: string[];
176
- visibleTools: string[]; // tool names visible to the LLM that iter
177
- }
199
+ `<Lens for={...}>` is just sugar over this internally — the recorder you'd write for Datadog is the same shape Lens uses.
178
200
 
179
- interface AgentToolInvocation {
180
- id: string;
181
- name: string;
182
- arguments: Record<string, unknown>;
183
- result: string;
184
- error?: boolean;
185
- decisionUpdate?: Record<string, unknown>;
186
- iterationIndex: number;
187
- turnIndex: number;
188
- durationMs?: number;
189
- }
201
+ ---
202
+
203
+ ## API reference
204
+
205
+ ### `useLens(factory)`
206
+
207
+ 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`.
208
+
209
+ ```ts
210
+ const agent = useLens(() => Agent.create(...).build());
211
+ const caller = useLens(() => LLMCall.create(...).build());
212
+ const rag = useLens(() => RAG.create(...).build());
190
213
  ```
191
214
 
192
- ---
215
+ ### `<Lens for={runner} />`
193
216
 
194
- ## Roadmap
217
+ The one-prop integration. Subscribes to the runner's events, watches its snapshot, renders both tabs.
195
218
 
196
- ### v0.1 (shipped)
197
- - `<AgentLens>` shell + MessagesPanel + IterationStrip + ToolCallInspector
198
- - Theme pass-through via FootprintTheme
199
- - `fromAgentSnapshot` adapter
219
+ | Prop | Type | Description |
220
+ |---|---|---|
221
+ | `for` | `Runner` (any agentfootprint runner) | The agent / caller / swarm / etc. to watch. |
222
+ | `theme` | `ThemeTokens?` | Optional — defaults to `coolDark`. |
223
+ | `appName` | `string?` | Optional brand label in the tab strip. |
200
224
 
201
- ### v0.2 — "why did the LLM decide that"
202
- - **Prompt Composer** — assembled system prompt per iteration with diff-across-iterations highlighting (base + skill body + instruction injections + message window + tool list).
203
- - **Decision Scope Ribbon** — horizontal pill timeline of decision scope fields; arrows back to the tool call that wrote each via `decisionUpdate`.
225
+ ### `runner.observe(handler)`
204
226
 
205
- ### v0.3 "is it shippable"
206
- - **Cost & Latency Attribution** — token burn curve, per-iter/per-tool breakdown, pluggable price table.
207
- - **Skill Dock** — loaded skills, activation state, tools-per-skill.
227
+ Subscribe to live events. Returns `() => void` (unsubscribe).
208
228
 
209
- ### v1.0 — "Lens v1"
210
- - Trace compare (two runs side-by-side)
211
- - Eval grid (batch runs → pass/fail matrix)
212
- - Exportable / importable traces via `TraceViewer` from explainable-ui
229
+ ```ts
230
+ const stop = agent.observe((event) => { /* ... */ });
231
+ // later:
232
+ stop();
233
+ ```
234
+
235
+ ### `runner.getSnapshot()`, `runner.getNarrativeEntries()`, `runner.getSpec()`
236
+
237
+ The standard agentfootprint introspection methods. `<Lens for={...}>` reads these automatically. You only call them yourself if you're building a custom UI.
238
+
239
+ ### `useLiveTimeline()` (escape hatch)
240
+
241
+ Returns `{ timeline, ingest, startTurn, reset, builder }`. Use when you want to feed Lens from a non-runner source (replayed logs, server-sent events, etc.).
213
242
 
214
243
  ---
215
244
 
216
- ## Design decisions
245
+ ## Why this design
246
+
247
+ **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.
248
+
249
+ This is the observer pattern, applied consistently across every agentfootprint runner. The outcome:
217
250
 
218
- - **Separate package, not a fork of explainable-ui.** Different audience (agent devs vs. pipeline/tool devs), different mental model (conversation loop vs. data-flow graph), different failure modes. Overloading explainable-ui would muddy both.
219
- - **Theme via FootprintTheme context, not a Lens-specific prop.** One source of truth; automatic propagation to the drill-in drawer; consumers don't learn two theme APIs.
220
- - **Adapter at the boundary.** `fromAgentSnapshot` is the single data-shape conversion. Panels render against the derived `AgentTimeline`, so agentfootprint's internal snapshot shape can evolve without breaking Lens.
221
- - **No new agentfootprint library changes required to use Lens.** Every field Lens needs is already emitted today (messages, commitLog, recorder snapshots, emit events).
251
+ - **One line to integrate** `<Lens for={agent} />`
252
+ - **Zero coupling** the agent doesn't know Lens exists
253
+ - **Composable** Lens + your telemetry + your logger all watch the same agent with no conflict
254
+ - **Uniform** any runner works with any observer
222
255
 
223
256
  ---
224
257
 
225
258
  ## License
226
259
 
227
- MIT © Sanjay Krishna Anbalagan
260
+ MIT