agentfootprint-lens 0.5.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.
Files changed (43) hide show
  1. package/README.md +181 -148
  2. package/dist/{chunk-7KGWX7RU.js → chunk-3N4WQXQP.js} +151 -6
  3. package/dist/chunk-3N4WQXQP.js.map +1 -0
  4. package/dist/{chunk-2OGMN63Q.js → chunk-VTJ6OQYJ.js} +648 -144
  5. package/dist/chunk-VTJ6OQYJ.js.map +1 -0
  6. package/dist/core.cjs +150 -5
  7. package/dist/core.cjs.map +1 -1
  8. package/dist/core.d.cts +121 -0
  9. package/dist/core.d.ts +121 -0
  10. package/dist/core.js +1 -1
  11. package/dist/index.cjs +793 -143
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +4 -2
  16. package/dist/react.cjs +793 -143
  17. package/dist/react.cjs.map +1 -1
  18. package/dist/react.d.cts +64 -4
  19. package/dist/react.d.ts +64 -4
  20. package/dist/react.js +4 -2
  21. package/package.json +1 -1
  22. package/dist/chunk-2JKRPWPZ.js +0 -3793
  23. package/dist/chunk-2JKRPWPZ.js.map +0 -1
  24. package/dist/chunk-2OGMN63Q.js.map +0 -1
  25. package/dist/chunk-2ZNJKUV7.js +0 -3781
  26. package/dist/chunk-2ZNJKUV7.js.map +0 -1
  27. package/dist/chunk-4LDHJAAY.js +0 -3772
  28. package/dist/chunk-4LDHJAAY.js.map +0 -1
  29. package/dist/chunk-7KGWX7RU.js.map +0 -1
  30. package/dist/chunk-EFG52PIH.js +0 -3783
  31. package/dist/chunk-EFG52PIH.js.map +0 -1
  32. package/dist/chunk-FAHWCPLM.js +0 -3930
  33. package/dist/chunk-FAHWCPLM.js.map +0 -1
  34. package/dist/chunk-FH3WCJCV.js +0 -3828
  35. package/dist/chunk-FH3WCJCV.js.map +0 -1
  36. package/dist/chunk-LOKXVQL6.js +0 -3822
  37. package/dist/chunk-LOKXVQL6.js.map +0 -1
  38. package/dist/chunk-OROO4WJY.js +0 -3821
  39. package/dist/chunk-OROO4WJY.js.map +0 -1
  40. package/dist/chunk-PPSBDRYH.js +0 -3793
  41. package/dist/chunk-PPSBDRYH.js.map +0 -1
  42. package/dist/chunk-ZDXQDQI4.js +0 -3822
  43. package/dist/chunk-ZDXQDQI4.js.map +0 -1
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
@@ -3,10 +3,30 @@ var LiveTimelineBuilder = class {
3
3
  constructor() {
4
4
  this.turns = [];
5
5
  this.currentTurn = null;
6
+ /**
7
+ * The most-recently-started iteration. Stays bound through the entire
8
+ * iteration lifecycle — llm_start opens it, llm_end ends the LLM phase
9
+ * but the iter stays current so subsequent `tool_start` / `tool_end`
10
+ * events (which fire AFTER llm_end in the agent loop) still attach to
11
+ * it. Cleared only on `commitCurrentTurn()` / `reset()`.
12
+ */
6
13
  this.currentIter = null;
14
+ /**
15
+ * True between an iteration's `llm_start` and its `llm_end`. Drives
16
+ * context-injection routing: events emitted while the LLM phase is
17
+ * active belong to THIS iteration's prompt; events emitted after
18
+ * `llm_end` belong to the NEXT iteration (they shape its context).
19
+ * Tool start/end ignore this flag — they always attach to the
20
+ * most-recent iter regardless of LLM phase.
21
+ */
22
+ this.llmPhaseActive = false;
7
23
  this.toolByCallId = /* @__PURE__ */ new Map();
8
24
  this.messages = [];
9
25
  this.finalDecision = {};
26
+ /** Context injections that fired BEFORE this turn's first llm_start —
27
+ * they shape iteration 1's context. Flushed onto iteration 1 at its
28
+ * llm_start. Reset at turn_start so each turn accumulates its own. */
29
+ this.pendingPreIterInjections = [];
10
30
  }
11
31
  /**
12
32
  * Begin a new turn. Call this BEFORE `agent.run(userPrompt)` so the
@@ -25,6 +45,7 @@ var LiveTimelineBuilder = class {
25
45
  totalDurationMs: 0,
26
46
  startMs: Date.now()
27
47
  };
48
+ this.pendingPreIterInjections = [];
28
49
  this.messages.push({ role: "user", content: userPrompt });
29
50
  }
30
51
  /**
@@ -42,6 +63,12 @@ var LiveTimelineBuilder = class {
42
63
  const e = event;
43
64
  if (!e || typeof e.type !== "string") return;
44
65
  switch (e.type) {
66
+ case "turn_start":
67
+ case "agentfootprint.agent.turn_start": {
68
+ const userMessage = e.userMessage ?? "";
69
+ this.startTurn(userMessage);
70
+ return;
71
+ }
45
72
  case "llm_start":
46
73
  case "agentfootprint.stream.llm_start":
47
74
  this.onLLMStart(e);
@@ -63,12 +90,39 @@ var LiveTimelineBuilder = class {
63
90
  this.commitCurrentTurn();
64
91
  return;
65
92
  default:
93
+ if (typeof e.type === "string" && e.type.startsWith("agentfootprint.context.")) {
94
+ this.onContextInjection(e.type, e);
95
+ }
66
96
  return;
67
97
  }
68
98
  }
99
+ /**
100
+ * Route a context-engineering event to the iteration it shaped.
101
+ *
102
+ * Routing rule: the `llmPhaseActive` flag (true between this iter's
103
+ * llm_start and llm_end) decides. While active, the event shaped THIS
104
+ * iter's prompt → attach directly. While inactive (between llm_end
105
+ * and the next llm_start, or before the first llm_start), the event
106
+ * is preparing context for the NEXT iter → queue on
107
+ * `pendingPreIterInjections`, flushed onto the next iter at its
108
+ * llm_start. Tool events do not gate on this flag — they bind to the
109
+ * most-recent iter unconditionally.
110
+ */
111
+ onContextInjection(rawName, e) {
112
+ if (!this.currentTurn) return;
113
+ const injection = buildInjection(rawName, e);
114
+ if (!injection) return;
115
+ if (this.currentIter && this.llmPhaseActive) {
116
+ this.currentIter.contextInjections.push(injection);
117
+ } else {
118
+ this.pendingPreIterInjections.push(injection);
119
+ }
120
+ }
69
121
  onLLMStart(e) {
70
122
  if (!this.currentTurn) return;
71
123
  const iterNum = e.iteration ?? this.currentTurn.iterations.length + 1;
124
+ const carriedInjections = this.pendingPreIterInjections;
125
+ this.pendingPreIterInjections = [];
72
126
  this.currentIter = {
73
127
  index: iterNum,
74
128
  assistantContent: "",
@@ -78,9 +132,11 @@ var LiveTimelineBuilder = class {
78
132
  startMs: Date.now(),
79
133
  // Freeze the message count here so "What Neo saw" can reproduce
80
134
  // the context window at this exact iteration later.
81
- messagesSentCount: this.messages.length
135
+ messagesSentCount: this.messages.length,
136
+ contextInjections: carriedInjections
82
137
  };
83
138
  this.currentTurn.iterations.push(this.currentIter);
139
+ this.llmPhaseActive = true;
84
140
  }
85
141
  onLLMEnd(e) {
86
142
  if (!this.currentIter || !this.currentTurn) return;
@@ -99,6 +155,7 @@ var LiveTimelineBuilder = class {
99
155
  if ((e.toolCallCount ?? 0) === 0) {
100
156
  this.currentTurn.finalContent = this.currentIter.assistantContent;
101
157
  }
158
+ this.llmPhaseActive = false;
102
159
  }
103
160
  onToolStart(e) {
104
161
  if (!this.currentIter || !this.currentTurn) return;
@@ -132,6 +189,7 @@ var LiveTimelineBuilder = class {
132
189
  this.turns.push(this.currentTurn);
133
190
  this.currentTurn = null;
134
191
  this.currentIter = null;
192
+ this.llmPhaseActive = false;
135
193
  }
136
194
  /**
137
195
  * Snapshot the current state as an immutable `AgentTimeline`. Safe to
@@ -143,12 +201,44 @@ var LiveTimelineBuilder = class {
143
201
  if (this.currentTurn) allTurns.push(this.currentTurn);
144
202
  const tools = [];
145
203
  const frozenTurns = allTurns.map((t) => {
204
+ const turnInjections = [];
205
+ const turnLedger = {};
146
206
  const iterations = t.iterations.map((i) => {
147
207
  const tcs = i.toolCalls.map((tc) => ({ ...tc }));
148
208
  tools.push(...tcs);
149
- return { ...i, toolCalls: tcs };
209
+ const contextInjections = i.contextInjections.map(
210
+ (ci) => ({ ...ci })
211
+ );
212
+ const contextLedger = {};
213
+ for (const ci of contextInjections) {
214
+ const d = ci.deltaCount;
215
+ if (!d) continue;
216
+ for (const [key, val] of Object.entries(d)) {
217
+ if (typeof val === "number") {
218
+ const prev = typeof contextLedger[key] === "number" ? contextLedger[key] : 0;
219
+ contextLedger[key] = prev + val;
220
+ const prevTurn = typeof turnLedger[key] === "number" ? turnLedger[key] : 0;
221
+ turnLedger[key] = prevTurn + val;
222
+ } else if (typeof val === "boolean") {
223
+ contextLedger[key] = contextLedger[key] === true || val;
224
+ turnLedger[key] = turnLedger[key] === true || val;
225
+ }
226
+ }
227
+ }
228
+ turnInjections.push(...contextInjections);
229
+ return {
230
+ ...i,
231
+ toolCalls: tcs,
232
+ contextInjections,
233
+ contextLedger
234
+ };
150
235
  });
151
- return { ...t, iterations };
236
+ return {
237
+ ...t,
238
+ iterations,
239
+ contextInjections: turnInjections,
240
+ contextLedger: turnLedger
241
+ };
152
242
  });
153
243
  return {
154
244
  turns: frozenTurns,
@@ -175,11 +265,56 @@ var LiveTimelineBuilder = class {
175
265
  this.turns = [];
176
266
  this.currentTurn = null;
177
267
  this.currentIter = null;
268
+ this.llmPhaseActive = false;
178
269
  this.toolByCallId.clear();
179
270
  this.messages = [];
180
271
  this.finalDecision = {};
272
+ this.pendingPreIterInjections = [];
181
273
  }
182
274
  };
275
+ function buildInjection(name, e) {
276
+ const suffix = name.slice("agentfootprint.context.".length);
277
+ const payload = e.payload;
278
+ const data = payload && typeof payload === "object" ? payload : e ?? {};
279
+ const role = typeof data.role === "string" ? data.role : void 0;
280
+ const targetIndex = typeof data.targetIndex === "number" ? data.targetIndex : void 0;
281
+ const deltaCount = data.deltaCount && typeof data.deltaCount === "object" ? data.deltaCount : void 0;
282
+ const enrich = (base) => ({
283
+ ...base,
284
+ ...role !== void 0 && { role },
285
+ ...targetIndex !== void 0 && { targetIndex },
286
+ ...deltaCount !== void 0 && { deltaCount },
287
+ payload: data
288
+ });
289
+ switch (suffix) {
290
+ case "rag.chunks": {
291
+ const chunkCount = Number(data.chunkCount ?? 0);
292
+ const topScore = typeof data.topScore === "number" ? data.topScore : void 0;
293
+ const label = chunkCount > 0 ? `${chunkCount} chunk${chunkCount === 1 ? "" : "s"}${topScore !== void 0 ? ` \xB7 top ${topScore.toFixed(2)}` : ""}` : "0 chunks";
294
+ return enrich({ source: "rag", slot: "messages", label });
295
+ }
296
+ case "skill.activated": {
297
+ const skillId = String(data.skillId ?? "skill");
298
+ return enrich({ source: "skill", slot: "system-prompt", label: skillId });
299
+ }
300
+ case "memory.injected": {
301
+ const count = Number(data.count ?? 0);
302
+ const label = count > 0 ? `memory \xB7 ${count} msg${count === 1 ? "" : "s"}` : "memory";
303
+ return enrich({ source: "memory", slot: "messages", label });
304
+ }
305
+ case "instructions.fired": {
306
+ const count = Number(data.count ?? (Array.isArray(data.ids) ? data.ids.length : 1));
307
+ const label = `${count} instruction${count === 1 ? "" : "s"}`;
308
+ return enrich({ source: "instructions", slot: "system-prompt", label });
309
+ }
310
+ default:
311
+ return enrich({
312
+ source: suffix.split(".")[0] || "context",
313
+ slot: "messages",
314
+ label: suffix
315
+ });
316
+ }
317
+ }
183
318
 
184
319
  // src/core/fromAgentSnapshot.ts
185
320
  function fromAgentSnapshot(runtime) {
@@ -353,7 +488,12 @@ function assembleTurns(messages, llmCalls, toolExecs, instructionEvals, toolReso
353
488
  decisionAtStart: {},
354
489
  // TODO(phase-2): derive from pre-iter commit
355
490
  ...instr?.matchedInstructions && { matchedInstructions: instr.matchedInstructions },
356
- visibleTools: visible?.visibleTools ?? []
491
+ visibleTools: visible?.visibleTools ?? [],
492
+ // Post-process path (snapshot-import — no live emit stream) has no
493
+ // way to reconstruct context-injection timing. Leave empty; the
494
+ // live path via LiveTimelineBuilder fills these in naturally.
495
+ contextInjections: [],
496
+ contextLedger: {}
357
497
  };
358
498
  currentTurn.iterations.push(iteration);
359
499
  if (!toolCalls.length) currentTurn.finalContent = iteration.assistantContent;
@@ -382,7 +522,12 @@ function finalizeTurn(t) {
382
522
  finalContent: t.finalContent,
383
523
  totalInputTokens,
384
524
  totalOutputTokens,
385
- totalDurationMs
525
+ totalDurationMs,
526
+ // Post-process snapshot path has no live emit timing, so the
527
+ // turn-level context fields stay empty here too — the live
528
+ // LiveTimelineBuilder path is the one that captures injections.
529
+ contextInjections: [],
530
+ contextLedger: {}
386
531
  };
387
532
  }
388
533
 
@@ -544,4 +689,4 @@ export {
544
689
  fromAgentSnapshot,
545
690
  deriveStages
546
691
  };
547
- //# sourceMappingURL=chunk-7KGWX7RU.js.map
692
+ //# sourceMappingURL=chunk-3N4WQXQP.js.map